Compare commits

...
78 Commits
Author SHA1 Message Date
CityFun f9f9169a4d fix(build): upgrade Go to 1.27.3 (#14161) 2026-10-22 15:41:34 +08:00
zhengkunwang223 1e01761ba3 fix(appstore): refresh cached resources and PHP versions
Version official app package and compose downloads using the synchronized store revision. Refresh outdated local resource caches and use synchronized database parameters for official PHP version options.
2026-10-22 11:53:36 +08:00
CityFun 15283fde34 feat: update deepseek default model (#14146) 2026-10-21 18:37:22 +08:00
Octopusandocto-patch 49f8e7f040 feat(ai-agents): refresh MiniMax Flash models and defaults (#14136)
Co-authored-by: octo-patch <octo-patch@users.noreply.github.com>
2026-10-21 18:23:53 +08:00
Yuki Guo 98b27af6a0 fix: container list search (#14134) 2026-10-21 18:22:02 +08:00
ssongliu a393224729 feat(ai): support benchmark node options and report integration (#14145) 2026-10-21 18:07:39 +08:00
Woolsen b851c84290 fix(container): reset sort to default when cleared (#14140)
The container list opens with name ascending but shows no sort indicator. Clearing the name sort kept the previous descending order, so two no-indicator states represented different orders. Reset the request to the default name ascending when the sort is cleared.
2026-10-21 18:01:18 +08:00
Woolsen 372855157d feat(container): support sorting by creation time (#14141)
Add a creation time column to the container list. The column uses the backend createdAt order field and displays the existing createTime value. It follows the standard three-state sort cycle, starting with newest first.
2026-10-21 18:01:07 +08:00
CityFunandssongliu 577d771495 feat(agent): add DeepSeek Harness and OctoP support (#14138)
* feat(agent): add DeepSeek Harness and OctoP support

* fix: restore default core edition configuration

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-21 17:25:38 +08:00
Yuki Guo ca3e6511e5 feat: ssl push multiple directories (#14137) 2026-10-21 17:23:56 +08:00
Yuki Guo 8af14eae25 feat: add container catalog batch upload (#14133) 2026-10-21 17:09:01 +08:00
ssongliu 175fbaf3f2 fix: correct terminal node routing for containers (#14142) 2026-10-21 16:40:42 +08:00
Yuki Guo 13199efd62 feat: add overview more quick menu (#14132) 2026-10-21 16:21:07 +08:00
HynoRandCursor 04099ae2a2 feat(terminal): add session grace expiry tests and improve detach logic (#14127)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 17:54:05 +08:00
ssongliu c695a2a408 fix(firewall): prepare access whitelist before startup (#14114) 2026-10-16 17:27:48 +08:00
ssongliu 690cb48280 feat(vllm): refine monitor snapshots and historical request counts (#14109) 2026-10-16 13:36:29 +08:00
ssongliu f226fed8cd feat(firewall): support restoring rule backups on startup (#14108) 2026-10-16 10:50:07 +08:00
Yuki Guo 110c292856 chore: chore home default css (#14101) 2026-10-15 15:45:25 +08:00
Kadxyandssongliu 8e865c2c46 fix: stop translating the built-in Default group name (#14100)
* fix: stop translating the built-in Default group name

* fix: resolve default cronjob group by flag

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-15 15:42:18 +08:00
Kadxy 17b8f472e7 fix(cronjob): show Yes/No text instead of check icon in script library interactive column (#14099) 2026-10-15 15:25:03 +08:00
Yuki Guo d9dc5d7aa9 fix: alert config log text (#14086)
* fix: alert config log text

* fix: alert config log text
2026-10-15 09:33:23 +08:00
ssongliu 547f9775b0 fix(firewall): support legacy ip RA checks and link documentation (#14083) 2026-10-14 15:57:07 +08:00
ssongliu f37afb1a37 fix: sync core panel port whitelist on agent startup (#14080) 2026-10-14 11:28:46 +08:00
CityFun 3b89d83554 feat(hermes-agent): support version upgrades (#14079) 2026-10-14 11:28:32 +08:00
蘭 12bfe6a1bd fix: Fix Webhook Response Judgment and Optimize Configuration and Testing Experience (#14077) 2026-10-14 11:28:21 +08:00
ssongliu aaf70cfc41 fix(log): improve operation logs (#14076) 2026-10-13 18:28:34 +08:00
Yuki Guo 1ecd4f7f0c fix: container log text (#14066) 2026-10-13 17:56:52 +08:00
CityFun a8ccdcade7 fix(ai-gateway): support backup logic during app upgrades (#14072) 2026-10-13 17:28:42 +08:00
ssongliu 4a89c0c031 fix: prevent refresh loop after unbinding current node license (#14064) 2026-10-13 11:20:48 +08:00
ssongliu f21ebb1a36 chore(deps): consolidate dependency and security updates (#14063) 2026-10-13 11:20:43 +08:00
ssongliu 3011383b07 fix(compose): handle relative paths and preserve multi-file projects (#14062) 2026-10-13 11:03:17 +08:00
Yuki Guo fd12866897 fix: fix website files (#14042)
* refactor: refactor website files

* refactor: refactor website files
2026-10-13 10:56:30 +08:00
ssongliu bc6cf519ce fix: improve database file handling (#14043) 2026-10-13 09:39:27 +08:00
ssongliu f9c43989fe fix: improve firewall compatibility (#14046) 2026-10-13 09:39:18 +08:00
Yuki Guo e90b201001 refactor: remove user tooltip (#14041) 2026-10-12 18:18:44 +08:00
ssongliu f7394938f7 chore(deps): update remaining dependencies and VueUse 15 (#14040) 2026-10-12 16:05:35 +08:00
ssongliu dada7294dd fix(firewall): preserve fail2ban priority when binding iptables chains (#14039)
Refs #14019
2026-10-12 15:54:55 +08:00
ssongliu afcb94f6ef feat(vllm): add request metrics and optimize monitor history (#14032) 2026-10-12 15:54:42 +08:00
ssongliu 5a2e430dff fix(home): calculate overview rates from actual sample intervals (#14033) 2026-10-12 15:52:34 +08:00
Yuki Guo f2ca4f56e6 feat: add Netcup DNS (#14038) 2026-10-12 15:03:07 +08:00
ssongliu b6f609aaee chore(deps): update dependencies and apply security patches (#14037) 2026-10-12 14:49:01 +08:00
Yuki Guo c960c0137d refactor: refactor user info (#14036) 2026-10-12 14:17:13 +08:00
Yuki Guo 09a901294e fix: local image upload package (#14031) 2026-10-12 10:08:07 +08:00
Yuki Guo 4d481a8f31 refactor: refactor website ssl delete text (#14024) 2026-10-12 10:07:58 +08:00
Yuki Guo f1e0aa3391 feat(container): support local image uploads and improve import task … (#14025)
* feat(container): support local image uploads and improve import task logs

* feat(container): support local image uploads and improve import task logs

* fix(container): remove duplicate image load response helper
2026-10-12 09:41:36 +08:00
Yuki Guo f475500543 fix(frontend): reset search filters and pagination on page visits (#14016) 2026-10-10 17:56:38 +08:00
Yuki Guo 8415a1ce28 refactor: refactor user info (#14021) 2026-10-10 17:50:05 +08:00
Yuki Guo faf4933a02 refactor: refactor website ssl keyType default value (#14023) 2026-10-10 17:49:14 +08:00
Yuki Guo c692a9f5e3 refactor: refactor website upload css (#14022)
* refactor: refactor website upload css

* refactor: refactor website upload css
2026-10-10 17:48:57 +08:00
CityFun e7f0c21715 fix(vllm): resolve stuck startup state blocking operations (#14017) 2026-10-10 11:13:17 +08:00
ssongliu da7e8f1dbf fix: Remove duplicate TaskScopeVm (#14020) 2026-10-10 10:59:22 +08:00
Yuki Guoandssongliu 486cd5d0ad feat(redis): enable remote terminal through tracked background tasks (#14012)
* feat(redis): enable remote terminal through tracked background tasks

* feat(redis): enable remote terminal through tracked background tasks

* refactor(redis): remove redundant CLI status tracking

* refactor(redis): retain existing task execution flow

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-09 18:20:51 +08:00
ssongliu 00ce5a7b36 fix(firewall): preserve IPv6 RA protection and simplify error messages (#14015) 2026-10-09 17:45:57 +08:00
ssongliu c17f055e06 fix: preserve VM task timeout cleanup and terminate command groups (#14008) 2026-10-09 17:45:41 +08:00
王贺 f22df4b29c fix(file): stabilize closable tab width to avoid layout jitter (#14013)
The closable card tabs animated the close icon width between active and inactive tabs, so each tab switch reflowed the tab bar and caused the page to jitter. Reserve the close icon slot and toggle it with opacity instead, matching the terminal tab implementation.
2026-10-09 17:16:10 +08:00
Yuki Guoandssongliu b9bab90a14 fix: fix clean network (#14009)
* fix: fix clean network

* refactor(container): simplify network cleanup

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-09 17:14:50 +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
418 changed files with 26292 additions and 16904 deletions
+16
View File
@@ -0,0 +1,16 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) AIBenchmarkSources(c *gin.Context) {
sources, err := service.AIBenchmarkSources(c.Query("kind"), c.Query("selected"))
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, sources)
}
+28 -8
View File
@@ -10,6 +10,9 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto"
"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/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/webhook_sender"
"github.com/gin-gonic/gin"
)
@@ -290,7 +293,7 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/update [post]
// @x-panel-log {"bodyKeys":["id","displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [id][displayName]","formatEN":"update alert config [id][displayName]"}
// @x-panel-log {"bodyKeys":["displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [displayName]","formatEN":"update alert config [displayName]"}
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
var req dto.AlertConfigUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -318,13 +321,15 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/status [post]
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [name][status]","formatEN":"update alert config status [name][status]"}
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
var req dto.AlertConfigStatusUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
name, err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c))
helper.SetOperationLogValues(c, map[string]string{"name": name})
if err != nil {
helper.InternalServer(c, err)
return
}
@@ -350,13 +355,14 @@ func loadAuditUser(c *gin.Context) string {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [id]","formatEN":"delete alert config [id]"}
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [name]","formatEN":"delete alert config [name]"}
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
var req dto.DeleteRequest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := alertService.DeleteAlertConfig(req.ID)
name, err := alertService.DeleteAlertConfig(req.ID)
helper.SetOperationLogValues(c, map[string]string{"name": name})
if err != nil {
helper.InternalServer(c, err)
return
@@ -378,10 +384,24 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
return
}
if req.Type == constant.Custom {
result, err := alertService.TestCustomAlertConfig(req)
lang := strings.TrimSpace(c.GetHeader("Accept-Language"))
if lang == "" {
lang = i18n.GetLanguageFromDB()
}
result, err := alertService.TestCustomAlertConfig(req, lang)
if err != nil {
helper.InternalServer(c, err)
return
if global.LOG != nil {
global.LOG.Warnf("webhook test preparation failed: configID=%d error=%q", req.ID,
webhook_sender.DiagnosticText(err.Error(), webhook_sender.Request{
Format: webhook_sender.BodyJSON, Body: []byte(req.Config),
}))
}
result = dto.AlertConfigTestResult{Success: false}
}
result.Response = ""
result.Message = ""
if !result.Success {
result.Message = i18n.GetMsgByKeyAndLang(lang, "ErrWebhookTestFailed")
}
helper.SuccessWithData(c, result)
return
+10 -4
View File
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
// @Tags Database Redis
// @Summary Install redis-cli
// @Success 200
// @Accept json
// @Param request body dto.RedisCliInstall true "request"
// @Success 200 {string} string
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/redis/install/cli [post]
func (b *BaseApi) InstallCli(c *gin.Context) {
if err := redisService.InstallCli(); err != nil {
var req dto.RedisCliInstall
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := redisService.InstallCli(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
helper.SuccessWithData(c, data)
}
// @Tags Database Redis
+80 -139
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":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则","formatEN":"update firewall rule"}
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":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序","formatEN":"reorder firewall rule"}
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
@@ -675,7 +556,7 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/policies/delete/batch [post]
// @x-panel-log {"bodyKeys":["uuids"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略 [uuids]","formatEN":"delete Docker port guard policies [uuids]"}
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略","formatEN":"delete Docker port guard policies"}
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
var request dto.DockerPortGuardPolicyBatchDelete
if err := helper.CheckBindAndValidate(&request, c); err != nil {
@@ -737,3 +618,63 @@ func handleDockerPortGuardError(c *gin.Context, err error) {
}
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
}
// @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)
}
+21
View File
@@ -3,6 +3,7 @@ package helper
import (
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
@@ -184,3 +185,23 @@ func GetParamInt32(paramName string, c *gin.Context) (int32, error) {
intNum, _ := strconv.Atoi(idParam)
return int32(intNum), nil
}
// SetOperationLogValues supplies server-resolved values to Core's operation log.
// Call before writing the response. Only pass public audit fields, never credentials.
func SetOperationLogValues(c *gin.Context, values map[string]string) {
const header = "X-Op-Resolved"
resolved := make(map[string]string)
if data, err := base64.RawURLEncoding.DecodeString(c.Writer.Header().Get(header)); err == nil {
_ = json.Unmarshal(data, &resolved)
}
if resolved == nil {
resolved = make(map[string]string)
}
for key, value := range values {
resolved[key] = value
}
data, err := json.Marshal(resolved)
if err == nil {
c.Header(header, base64.RawURLEncoding.EncodeToString(data))
}
}
+10 -23
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(c.Request.Context(), req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) LoadVLLMCurrent(c *gin.Context) {
var req dto.MonitorVLLMCurrent
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := monitorService.LoadVLLMCurrent(c.Request.Context(), req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) CleanVLLMMonitor(c *gin.Context) {
var req dto.MonitorVLLMClean
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := monitorService.CleanVLLMMonitor(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+84
View File
@@ -0,0 +1,84 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
// @Tags Dashboard
// @Summary List quick directories
// @Success 200 {array} model.QuickDirectory
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory [get]
func (b *BaseApi) ListQuickDirectories(c *gin.Context) {
items, err := dashboardService.ListQuickDirectories()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, items)
}
// @Tags Dashboard
// @Summary Create quick directory
// @Accept json
// @Param request body dto.QuickDirectoryCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory [post]
func (b *BaseApi) CreateQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
item, err := dashboardService.CreateQuickDirectory(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, item)
}
// @Tags Dashboard
// @Summary Update quick directory
// @Accept json
// @Param request body dto.QuickDirectoryUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory/update [post]
func (b *BaseApi) UpdateQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
item, err := dashboardService.UpdateQuickDirectory(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, item)
}
// @Tags Dashboard
// @Summary Delete quick directory
// @Accept json
// @Param request body dto.QuickDirectoryDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory/del [post]
func (b *BaseApi) DeleteQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := dashboardService.DeleteQuickDirectory(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+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)
+3 -1
View File
@@ -9,7 +9,8 @@ type AgentCreateReq struct {
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
BridgePort int `json:"bridgePort"`
AllowedOrigins []string `json:"allowedOrigins"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent deepseek-harness octop"`
AccessHost string `json:"accessHost"`
Model string `json:"model"`
AccountID uint `json:"accountId"`
Token string `json:"token"`
@@ -110,6 +111,7 @@ type AgentBatchOperateResult struct {
}
type AgentItem struct {
AccessHost string `json:"accessHost"`
ID uint `json:"id"`
Name string `json:"name"`
Remark string `json:"remark"`
+13
View File
@@ -0,0 +1,13 @@
package dto
type AIBenchmarkSource struct {
ID string `json:"id"`
Kind string `json:"kind"`
Name string `json:"name"`
Status string `json:"status"`
BaseURL string `json:"baseUrl"`
Endpoint string `json:"endpoint"`
Models []string `json:"models"`
Tokenizer string `json:"tokenizer"`
Message string `json:"message"`
}
+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"`
+21 -1
View File
@@ -31,7 +31,27 @@ type DashboardBase struct {
}
type ChangeQuicks struct {
Quicks []QuickJump `json:"quicks"`
Quicks []QuickVisibility `json:"quicks" validate:"required,dive"`
}
type QuickVisibility struct {
ID uint `json:"id" validate:"required"`
IsShow bool `json:"isShow"`
}
type QuickDirectoryCreate struct {
Path string `json:"path" validate:"required"`
Alias string `json:"alias"`
}
type QuickDirectoryDelete struct {
ID uint `json:"id" validate:"required"`
Legacy bool `json:"legacy"`
}
type QuickDirectoryUpdate struct {
QuickDirectoryDelete
QuickDirectoryCreate
}
type QuickJump struct {
+4
View File
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
Port int64 `json:"port"`
}
type RedisCliInstall struct {
TaskID string `json:"taskID" validate:"omitempty,uuid"`
}
// mysql
type MysqlDBSearch struct {
PageInfo
+92 -127
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,13 +20,13 @@ 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"`
}
type FirewallLifecycleOperation struct {
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
WithDockerRestart bool `json:"withDockerRestart"`
}
@@ -49,12 +51,12 @@ type FirewallBackendOption struct {
}
type FirewallBackendFamilyStatus struct {
Available bool `json:"available"`
Initialized bool `json:"initialized"`
Bound bool `json:"bound"`
Reason string `json:"reason,omitempty"`
ForwardPolicy string `json:"forwardPolicy,omitempty"`
RAInterfaces []string `json:"raInterfaces,omitempty"`
Partial bool `json:"partial"`
Available bool `json:"available"`
Initialized bool `json:"initialized"`
Bound bool `json:"bound"`
Reason string `json:"reason,omitempty"`
RAInterfaces []string `json:"raInterfaces,omitempty"`
}
type FirewallBackendGroup struct {
@@ -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 {
+94 -18
View File
@@ -1,6 +1,11 @@
package dto
import "time"
import (
"encoding/json"
"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 +31,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 +69,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 +126,34 @@ type GPUProcess struct {
ProcessName string `json:"processName"`
UsedMemory string `json:"usedMemory"`
}
type MonitorVLLMSearch struct {
Group string `json:"group" validate:"omitempty,oneof=overview latency requests"`
AppInstallID uint `json:"appInstallID" validate:"required"`
StartTime time.Time `json:"startTime" validate:"required"`
EndTime time.Time `json:"endTime" validate:"required"`
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
}
type MonitorVLLMData struct {
SampleCount int64 `json:"sampleCount"`
BucketSeconds int64 `json:"bucketSeconds"`
Points []model.MonitorVLLM `json:"points"`
}
type MonitorVLLMCurrent struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorVLLMCurrentData struct {
model.MonitorVLLM
Metrics map[string]json.RawMessage `json:"metrics,omitempty"`
}
type MonitorVLLMClean struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorClean struct {
Type string `json:"type" validate:"required,oneof=host gpu"`
}
+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"`
+7
View File
@@ -5,6 +5,13 @@ type AppLauncher struct {
Key string `json:"key"`
}
type QuickDirectory struct {
BaseModel
Path string `json:"path"`
Alias string `json:"alias"`
Legacy bool `json:"legacy" gorm:"-"`
}
type QuickJump struct {
BaseModel
Name string `json:"name"`
-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"`
}
+85 -9
View File
@@ -1,5 +1,7 @@
package model
import "time"
type MonitorBase struct {
BaseModel
Cpu float64 `json:"cpu"`
@@ -33,14 +35,88 @@ 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:"-"`
IntervalSeconds float64 `json:"intervalSeconds" gorm:"-"`
SampleIntervalSeconds int `json:"-"`
RequestDeltas string `json:"-"`
CompletedRequests *float64 `json:"completedRequests" gorm:"-"`
QueueTime *float64 `json:"queueTime"`
AverageGenerationTokens *float64 `json:"averageGenerationTokens"`
PrefixCacheHitRate *float64 `json:"prefixCacheHitRate"`
FinishReasons map[string]float64 `json:"finishReasons" gorm:"-"`
PromptDistribution map[string]float64 `json:"promptDistribution" gorm:"-"`
GenerationDistribution map[string]float64 `json:"generationDistribution" gorm:"-"`
Running *float64 `json:"running"`
Waiting *float64 `json:"waiting"`
CacheUsage *float64 `json:"cacheUsage"`
PromptThroughput *float64 `json:"promptThroughput"`
GenerationThroughput *float64 `json:"generationThroughput"`
RequestThroughput *float64 `json:"requestThroughput"`
TimeToFirstToken *float64 `json:"timeToFirstToken"`
TimePerOutputToken *float64 `json:"timePerOutputToken"`
RequestLatency *float64 `json:"requestLatency"`
PrefillTime *float64 `json:"prefillTime"`
DecodeTime *float64 `json:"decodeTime"`
TimeToFirstTokenP50 *float64 `json:"timeToFirstTokenP50"`
TimeToFirstTokenP90 *float64 `json:"timeToFirstTokenP90"`
TimeToFirstTokenP95 *float64 `json:"timeToFirstTokenP95"`
TimeToFirstTokenP99 *float64 `json:"timeToFirstTokenP99"`
TimePerOutputTokenP50 *float64 `json:"timePerOutputTokenP50"`
TimePerOutputTokenP90 *float64 `json:"timePerOutputTokenP90"`
TimePerOutputTokenP95 *float64 `json:"timePerOutputTokenP95"`
TimePerOutputTokenP99 *float64 `json:"timePerOutputTokenP99"`
RequestLatencyP50 *float64 `json:"requestLatencyP50"`
RequestLatencyP90 *float64 `json:"requestLatencyP90"`
RequestLatencyP95 *float64 `json:"requestLatencyP95"`
RequestLatencyP99 *float64 `json:"requestLatencyP99"`
}
+2 -3
View File
@@ -66,7 +66,7 @@ var catalog = map[string]Meta{
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
},
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
Models: []Model{{ID: "deepseek-flash", Name: "deepseek-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
},
"bailian-coding-plan": {
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
@@ -82,7 +82,6 @@ var catalog = map[string]Meta{
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
{ID: "glm-5", Name: "GLM-5"},
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
},
@@ -114,7 +113,7 @@ var catalog = map[string]Meta{
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
},
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
Models: []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}},
},
"xiaomi": {
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
+100
View File
@@ -0,0 +1,100 @@
package provider
import (
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
)
func TestMiniMaxCatalog(t *testing.T) {
meta, ok := Get("minimax")
if !ok || !reflect.DeepEqual(meta.Models, []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}}) {
t.Fatalf("unexpected MiniMax models: %+v", meta.Models)
}
if meta.DefaultAPIType != "anthropic-messages" {
t.Fatalf("changed default protocol: %s", meta.DefaultAPIType)
}
for _, provider := range All() {
for _, model := range provider.Models {
if strings.Contains(strings.ToLower(model.ID), "minimax") && model.ID != "MiniMax-M3.1-Flash" && model.ID != "MiniMax-M3" {
t.Errorf("legacy suggestion in %s: %s", provider.Key, model.ID)
}
}
}
}
func TestMiniMaxVerificationRequests(t *testing.T) {
for _, region := range []string{"https://api.minimaxi.com", "https://api.minimax.io"} {
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
for _, auth := range []string{AuthModeXAPIKey, AuthModeBearer} {
for _, model := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "custom-model"} {
t.Run(region+"/"+protocol+"/"+auth+"/"+model, func(t *testing.T) {
base, endpoint := region+"/v1", "/chat/completions"
if protocol == "anthropic-messages" {
base, endpoint = region+"/anthropic", "/v1/messages"
}
built := BuildVerifyRequest("minimax", protocol, auth, base, "test-key", model)
if built.URL != base+endpoint {
t.Fatalf("URL = %s", built.URL)
}
var captured map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != strings.TrimPrefix(base, region)+endpoint {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
if protocol == "anthropic-messages" && auth == AuthModeXAPIKey {
if r.Header.Get("x-api-key") != "test-key" || r.Header.Get("Authorization") != "" {
t.Error("incorrect API key authentication")
}
} else if r.Header.Get("Authorization") != "Bearer test-key" {
t.Error("incorrect bearer authentication")
}
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
t.Error(err)
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
if err := VerifyAccount("minimax", protocol, auth, server.URL+strings.TrimPrefix(base, region), "test-key", model); err != nil {
t.Fatal(err)
}
if captured["model"] != model || captured["stream"] != false || captured["max_tokens"] != float64(defaultVerifyMaxTokens) {
t.Fatalf("unexpected serialized body: %#v", captured)
}
if model == "MiniMax-M3.1-Flash" {
if protocol == "anthropic-messages" {
if !reflect.DeepEqual(captured["thinking"], map[string]interface{}{"type": "adaptive"}) || !reflect.DeepEqual(captured["output_config"], map[string]interface{}{"effort": "max"}) || captured["reasoning_effort"] != nil {
t.Fatalf("invalid Anthropic Flash reasoning: %#v", captured)
}
} else if captured["reasoning_effort"] != "max" || captured["thinking"] != nil || captured["output_config"] != nil {
t.Fatalf("invalid OpenAI Flash reasoning: %#v", captured)
}
} else if captured["reasoning_effort"] != nil || captured["thinking"] != nil || captured["output_config"] != nil {
t.Fatalf("changed legacy/custom reasoning: %#v", captured)
}
})
}
}
}
}
}
func TestMiniMaxExplicitGatewayAndCustomModels(t *testing.T) {
for _, provider := range []string{"bailian-coding-plan", "custom", "openrouter"} {
for _, protocol := range []string{"openai-completions", "anthropic-messages"} {
for _, model := range []string{"MiniMax-M2.5", "MiniMax-M3.1-Flash", "my-custom-model"} {
request := BuildVerifyRequest(provider, protocol, AuthModeBearer, "https://example.test/v1", "test-key", model)
var body map[string]interface{}
if err := json.Unmarshal(request.Body, &body); err != nil {
t.Fatal(err)
}
if body["model"] != model || body["reasoning_effort"] != nil || body["output_config"] != nil || body["thinking"] != nil {
t.Fatalf("changed explicit %s request: %#v", provider, body)
}
}
}
}
}
+13 -4
View File
@@ -118,10 +118,15 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
headers["x-api-key"] = apiKey
}
headers["anthropic-version"] = "2023-06-01"
request.Body = mustJSON(map[string]interface{}{
body := map[string]interface{}{
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
})
}
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
body["thinking"] = map[string]string{"type": "adaptive"}
body["output_config"] = map[string]string{"effort": "max"}
}
request.Body = mustJSON(body)
case "openai-responses":
request.URL = baseURL + "/responses"
headers["Authorization"] = "Bearer " + apiKey
@@ -131,9 +136,13 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
headers["Authorization"] = "Bearer " + apiKey
}
request.Body = mustJSON(map[string]interface{}{
body := map[string]interface{}{
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
})
}
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
body["reasoning_effort"] = "max"
}
request.Body = mustJSON(body)
}
return request
}
+69 -3
View File
@@ -3,6 +3,7 @@ package repo
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type LauncherRepo struct{}
@@ -17,6 +18,11 @@ type ILauncherRepo interface {
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
ListQuickJump(withAll bool) []model.QuickJump
UpdateQuicks(quicks []model.QuickJump) error
ListQuickDirectories() ([]model.QuickDirectory, error)
CountQuickDirectories() (int64, error)
CreateQuickDirectory(item *model.QuickDirectory) error
UpdateQuickDirectory(item *model.QuickDirectory) error
DeleteQuickDirectory(id uint, legacy bool) error
}
func NewILauncherRepo() ILauncherRepo {
@@ -95,13 +101,73 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
for _, item := range quicks {
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
"is_show": item.IsShow,
"detail": item.Detail,
"alias": item.Alias,
}).Error; err != nil {
tx.Rollback()
return err
}
}
tx.Commit()
return tx.Commit().Error
}
// Legacy entries remain in the overview configuration until explicitly deleted.
func legacyQuickDirectories(db *gorm.DB) *gorm.DB {
return db.Model(&model.QuickJump{}).Where("name = ? AND is_show = ? AND detail <> ?", "File", true, "")
}
func (u *LauncherRepo) ListQuickDirectories() ([]model.QuickDirectory, error) {
items := make([]model.QuickDirectory, 0)
if err := global.DB.Order("id ASC").Find(&items).Error; err != nil {
return nil, err
}
var quicks []model.QuickJump
if err := legacyQuickDirectories(global.DB).Order("id ASC").Find(&quicks).Error; err != nil {
return nil, err
}
for _, quick := range quicks {
items = append(items, model.QuickDirectory{BaseModel: quick.BaseModel, Path: quick.Detail, Alias: quick.Alias, Legacy: true})
}
return items, nil
}
func (u *LauncherRepo) CountQuickDirectories() (int64, error) {
var count, legacyCount int64
if err := global.DB.Model(&model.QuickDirectory{}).Count(&count).Error; err != nil {
return 0, err
}
if err := legacyQuickDirectories(global.DB).Count(&legacyCount).Error; err != nil {
return 0, err
}
return count + legacyCount, nil
}
func (u *LauncherRepo) CreateQuickDirectory(item *model.QuickDirectory) error {
return global.DB.Create(item).Error
}
func (u *LauncherRepo) UpdateQuickDirectory(item *model.QuickDirectory) error {
var result *gorm.DB
if item.Legacy {
result = legacyQuickDirectories(global.DB).Where("id = ?", item.ID).Updates(map[string]interface{}{"detail": item.Path, "alias": item.Alias})
} else {
result = global.DB.Model(&model.QuickDirectory{}).Where("id = ?", item.ID).Updates(map[string]interface{}{"path": item.Path, "alias": item.Alias})
}
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (u *LauncherRepo) DeleteQuickDirectory(id uint, legacy bool) error {
var result *gorm.DB
if legacy {
result = legacyQuickDirectories(global.DB).Where("id = ?", id).Updates(map[string]interface{}{"detail": nil, "alias": nil})
} else {
result = global.DB.Delete(&model.QuickDirectory{}, id)
}
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
-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
}
+132
View File
@@ -0,0 +1,132 @@
package repo
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type VLLMMonitorRepo struct{}
type VLLMHistoryPoint struct {
model.MonitorVLLM
Bucket int64
CompletedRequests *float64
}
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
return global.VLLMMonitorDB.Create(point).Error
}
func (r *VLLMMonitorRepo) Latest(id uint) (model.MonitorVLLM, error) {
var point model.MonitorVLLM
db := global.VLLMMonitorDB.Where("app_install_id = ?", id)
err := db.Order("created_at DESC, id DESC").First(&point).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return point, nil
}
return point, err
}
func (r *VLLMMonitorRepo) CleanTarget(id uint) error {
return global.VLLMMonitorDB.Where("app_install_id = ?", id).Delete(&model.MonitorVLLM{}).Error
}
func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
}
func (r *VLLMMonitorRepo) Count(ctx context.Context, id uint, start, end time.Time) (int64, error) {
var count int64
db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
err := db.Count(&count).Error
return count, err
}
func (r *VLLMMonitorRepo) History(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string) ([]VLLMHistoryPoint, error) {
db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
var metrics []string
if group == "" || group == "overview" {
metrics = append(metrics, "running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput")
if seconds <= 0 || aggregation == "max" {
metrics = append(metrics, "prefix_cache_hit_rate")
}
}
if seconds <= 0 || aggregation == "max" {
if group == "" || group == "latency" {
metrics = append(metrics, "queue_time", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99")
}
if group == "" || group == "requests" {
metrics = append(metrics, "average_generation_tokens")
}
}
var columns []string
if seconds > 0 {
operation := "AVG"
if aggregation == "max" {
operation = "MAX"
}
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
for _, column := range metrics {
columns = append(columns, operation+"("+column+") AS "+column)
}
db = db.Group("bucket").Order("bucket ASC")
} else {
columns = append([]string{"id", "created_at", "app_install_id", "status", "sample_interval_seconds"}, metrics...)
if group == "" || group == "requests" {
columns = append(columns, "histogram_deltas", "request_deltas")
}
db = db.Order("created_at ASC, id ASC")
}
if group == "" || group == "overview" {
count := "CASE WHEN json_valid(request_deltas) THEN CASE WHEN json_extract(request_deltas, '$.seconds') > 0 THEN ROUND(request_throughput * json_extract(request_deltas, '$.seconds')) END END"
if seconds > 0 {
count = "SUM(" + count + ")"
}
columns = append(columns, count+" AS completed_requests")
}
var points []VLLMHistoryPoint
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
return points, err
}
func (r *VLLMMonitorRepo) WalkDeltas(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string, visit func(int64, string, string) error) error {
histograms, requests := "''", "''"
if group == "" || group == "requests" || (group == "latency" && aggregation != "max") {
histograms = "COALESCE(histogram_deltas, '')"
}
if group == "" || group == "requests" || (group == "overview" && aggregation != "max") {
requests = "COALESCE(request_deltas, '')"
}
if histograms == "''" && requests == "''" {
return ctx.Err()
}
bucket := fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d", start.Unix(), seconds)
rows, err := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).
Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end).
Select(bucket + ", " + histograms + ", " + requests).Rows()
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
if err := ctx.Err(); err != nil {
return err
}
var index int64
var histograms, requests string
if err := rows.Scan(&index, &histograms, &requests); err != nil {
return err
}
if err := visit(index, histograms, requests); err != nil {
return err
}
}
return rows.Err()
}
+65
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{}
@@ -14,17 +16,22 @@ type ISettingRepo interface {
GetList(opts ...DBOption) ([]model.Setting, error)
Get(opts ...DBOption) (model.Setting, error)
GetValueByKey(key string) (string, error)
GetCoreValueByKey(key string) (string, error)
Create(key, value string) error
Update(key, value string) error
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
}
@@ -68,6 +75,17 @@ func (s *SettingRepo) GetValueByKey(key string) (string, error) {
return setting.Value, nil
}
func (s *SettingRepo) GetCoreValueByKey(key string) (string, error) {
if global.CoreDB == nil {
return "", errors.New("core database is unavailable")
}
var setting model.Setting
if err := global.CoreDB.Where("key = ?", key).First(&setting).Error; err != nil {
return "", err
}
return setting.Value, nil
}
func (s *SettingRepo) WithByKey(key string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("key = ?", key)
@@ -90,6 +108,25 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) error {
return global.DB.Model(&setting).UpdateColumn("value", value).Error
}
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 +148,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) }
}
+4 -1
View File
@@ -2,6 +2,7 @@ package repo
import (
"context"
"time"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -110,7 +111,9 @@ func (t TaskRepo) Update(ctx context.Context, task *model.Task) error {
}
func (t TaskRepo) UpdateRunningTaskToFailed() error {
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{"status": constant.StatusFailed, "error_msg": "1Panel restart causes failure"}).Error
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{
"status": constant.StatusFailed, "error_msg": constant.InterruptedMsg, "end_at": time.Now(),
}).Error
}
func (t TaskRepo) CountExecutingTask() (int64, error) {
+12 -1
View File
@@ -142,6 +142,9 @@ const (
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
agentType := req.AgentType
if err := validateNativeAgentCreate(req); err != nil {
return nil, err
}
if err := checkPortExist(req.WebUIPort); err != nil {
return nil, err
}
@@ -237,6 +240,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
}
} else if agentType == constant.AppCopaw {
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
} else if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOctop {
dashboardAuth = agentDashboardAuth{Username: req.DashboardUsername, Password: req.DashboardPassword}
}
params := map[string]interface{}{
@@ -245,6 +250,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
constant.HostIP: "",
}
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
if agentType == constant.AppDeepSeekHarness {
params["HTTPS_ACCESS_HOST"] = req.AccessHost
}
if agentType == constant.AppOctop {
params["OCTOP_LOG_LEVEL"] = "info"
}
if agentType == constant.AppOpenclaw {
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
params["ALLOWED_ORIGIN"] = allowedOrigin
@@ -738,7 +749,7 @@ func requiresAgentAccount(agentType string) bool {
}
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
params["PANEL_APP_PORT_HTTPS"] = webUIPort
return
}
+115
View File
@@ -0,0 +1,115 @@
package service
import (
"encoding/json"
"path/filepath"
"reflect"
"testing"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
)
func TestMiniMaxOpenclawModelMetadata(t *testing.T) {
for _, provider := range []string{"minimax", "custom", "bailian-coding-plan"} {
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "saved-custom"} {
t.Run(provider+"/"+protocol+"/"+id, func(t *testing.T) {
account := &model.AgentAccount{Provider: provider, APIType: protocol, BaseURL: "https://example.test/v1", APIKey: "test-key"}
_, entry, _, _, err := buildOpenclawAccountModelConfig(account, dto.AgentAccountModel{ID: id, Name: "Saved name"})
if err != nil {
t.Fatal(err)
}
payload, err := json.Marshal(entry)
if err != nil {
t.Fatal(err)
}
var actual modelEntry
if err := json.Unmarshal(payload, &actual); err != nil {
t.Fatal(err)
}
if actual.ID != id || actual.Name != "Saved name" {
t.Fatalf("changed saved identity: %s", payload)
}
if provider == "minimax" && id == "MiniMax-M3.1-Flash" {
if actual.ContextWindow != 1000000 || actual.MaxTokens != 524288 || !reflect.DeepEqual(actual.Input, []string{"text", "image"}) {
t.Fatalf("incorrect direct Flash metadata: %s", payload)
}
} else if actual.ContextWindow != 0 || actual.MaxTokens != 0 {
t.Fatalf("changed existing/gateway limits: %s", payload)
}
})
}
}
}
}
func TestMiniMaxPreservesSavedOpenclawMetadata(t *testing.T) {
current := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text"}, ContextWindow: 32000, MaxTokens: 4096}}},
}}
conf := map[string]interface{}{}
if err := applyOpenclawModelsConfig(conf, current); err != nil {
t.Fatal(err)
}
next := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text", "image"}, ContextWindow: 1000000, MaxTokens: 524288}}},
}}
preserveOpenclawModelMetadata(conf, next)
if !reflect.DeepEqual(next.Providers["minimax"].Models, current.Providers["minimax"].Models) {
t.Fatalf("saved metadata changed: %+v", next)
}
}
func TestMiniMaxDefaultAccountRequest(t *testing.T) {
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
account := &model.AgentAccount{Provider: "minimax", APIType: protocol}
models, err := buildInitialAgentAccountModels(account, nil)
if err != nil {
t.Fatal(err)
}
selected, err := resolveAgentAccountVerifyModel("minimax", "", models)
if err != nil || selected != "MiniMax-M3.1-Flash" || len(models) != 2 || models[1].ID != "MiniMax-M3" {
t.Fatalf("unexpected default: %s, %+v, %v", selected, models, err)
}
request := providercatalog.BuildVerifyRequest("minimax", protocol, "", "https://example.test", "test-key", selected)
var body map[string]interface{}
if err := json.Unmarshal(request.Body, &body); err != nil || body["model"] != selected {
t.Fatalf("default request: %s, %v", request.Body, err)
}
}
}
func TestMiniMaxHermesConfiguration(t *testing.T) {
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "my-custom-model"} {
dir := t.TempDir()
account := &model.AgentAccount{Provider: "minimax", APIType: protocol, BaseURL: "https://api.minimaxi.com/anthropic", APIKey: "test-key"}
if err := writeHermesConfig(dir, account, id, ""); err != nil {
t.Fatal(err)
}
conf, err := readHermesConfig(filepath.Join(dir, "config.yaml"))
if err != nil {
t.Fatal(err)
}
if conf.Model.Default != id || conf.Model.Provider != "minimax-cn" || conf.Model.BaseURL != account.BaseURL {
t.Fatalf("changed Hermes selection: %+v", conf.Model)
}
}
}
}
func TestMiniMaxExplicitAccountModels(t *testing.T) {
for _, provider := range []string{"minimax", "bailian-coding-plan", "custom"} {
account := &model.AgentAccount{Provider: provider, APIType: "openai-completions"}
requested := []dto.AgentAccountModel{{ID: "MiniMax-M2.5", Name: "Saved legacy"}, {ID: "my-custom-model", Name: "Saved custom"}}
actual, err := buildInitialAgentAccountModels(account, requested)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(actual, requested) {
t.Fatalf("changed explicit %s models: %+v", provider, actual)
}
}
}
+69
View File
@@ -0,0 +1,69 @@
package service
import (
"net"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
)
var deepSeekUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
var deepSeekHostLabelPattern = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$`)
func isDeepSeekAccessHost(value string) bool {
if ip := net.ParseIP(value); ip != nil {
return ip.To4() != nil && !strings.Contains(value, ":")
}
if value == "" || len(value) > 253 || strings.Trim(value, "0123456789.") == "" {
return false
}
for _, label := range strings.Split(value, ".") {
if !deepSeekHostLabelPattern.MatchString(label) {
return false
}
}
return true
}
func validateNativeAgentCreate(req dto.AgentCreateReq) error {
if req.AgentType != constant.AppDeepSeekHarness && req.AgentType != constant.AppOctop {
return nil
}
if strings.TrimSpace(req.DashboardUsername) == "" || strings.ContainsAny(req.DashboardUsername, "\r\n\x00") {
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
}
if strings.ContainsAny(req.DashboardPassword, "\r\n\x00") || req.DashboardPassword != strings.TrimSpace(req.DashboardPassword) {
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
if req.AgentType == constant.AppDeepSeekHarness {
if !isDeepSeekAccessHost(req.AccessHost) {
return buserr.WithDetail("ErrInvalidParams", "accessHost", nil)
}
if !deepSeekUsernamePattern.MatchString(req.DashboardUsername) {
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
}
if utf8.RuneCountInString(req.DashboardPassword) < 12 {
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
return nil
}
hasLetter, hasDigit := false, false
for _, char := range req.DashboardPassword {
hasLetter = hasLetter || unicode.IsLetter(char)
hasDigit = hasDigit || unicode.IsDigit(char)
}
if utf8.RuneCountInString(req.DashboardPassword) < 8 || !hasLetter || !hasDigit {
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
switch strings.ToLower(req.DashboardPassword) {
case "password", "password1", "password12", "password123", "12345678", "123456789", "qwerty123",
"admin123", "welcome1", "letmein1", "changeme1", "octop123", "abc12345", "iloveyou1":
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
return nil
}
+25 -6
View File
@@ -415,6 +415,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
} else {
item.WebUIPort = appInstall.HttpPort
}
if agentType == constant.AppDeepSeekHarness {
item.WebUIPort = appInstall.HttpsPort
if values, err := readAgentEnvMap(appInstall.GetEnvPath()); err == nil {
accessHost := values["HTTPS_ACCESS_HOST"]
if isDeepSeekAccessHost(accessHost) {
item.AccessHost = accessHost
}
}
}
item.Path = appInstall.GetPath()
item.Status = appInstall.Status
item.Message = appInstall.Message
@@ -442,7 +451,8 @@ func localizedAgentProviderName(provider string) string {
}
func isAgentAppKey(appKey string) bool {
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent ||
appKey == constant.AppDeepSeekHarness || appKey == constant.AppOctop
}
func isOpenclawLegacyHTTPVersion(version string) bool {
@@ -652,7 +662,7 @@ func checkAgentUpgradable(install model.AppInstall) bool {
if len(ignores) > 0 {
continue
}
if common.IsCrossVersion(install.Version, item.Version) && !install.App.CrossVersionUpdate {
if isCrossAppVersion(install.App.Key, install.Version, item.Version) && !install.App.CrossVersionUpdate {
continue
}
versions = append(versions, item.Version)
@@ -660,12 +670,12 @@ func checkAgentUpgradable(install model.AppInstall) bool {
if len(versions) == 0 {
return false
}
versions = common.GetSortedVersions(versions)
versions = sortAppVersions(install.App.Key, versions)
lastVersion := versions[0]
if common.IsCrossVersion(install.Version, lastVersion) {
if isCrossAppVersion(install.App.Key, install.Version, lastVersion) {
return install.App.CrossVersionUpdate
}
return common.CompareVersion(lastVersion, install.Version)
return isNewerAppVersion(install.App.Key, lastVersion, install.Version)
}
type openclawConfig struct {
@@ -1253,7 +1263,12 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
if err != nil {
return "", modelEntry{}, "", modelProvider{}, err
}
return providerPatch.PrimaryModel, buildOpenclawModelEntry(providerPatch.ModelID, model), providerPatch.ProviderKey, modelProvider{
entry := buildOpenclawModelEntry(providerPatch.ModelID, model)
if account.Provider == "minimax" && providerPatch.ModelID == "MiniMax-M3.1-Flash" {
entry.ContextWindow = 1000000
entry.MaxTokens = 524288
}
return providerPatch.PrimaryModel, entry, providerPatch.ProviderKey, modelProvider{
ApiKey: providerPatch.APIKey,
BaseUrl: providerPatch.BaseURL,
Api: providerPatch.APIType,
@@ -1641,6 +1656,10 @@ func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
case constant.AppCopaw:
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
case constant.AppDeepSeekHarness:
return "DSH_AUTH_USERNAME", "DSH_AUTH_PASSWORD", true
case constant.AppOctop:
return "OCTOP_ADMIN_USERNAME", "OCTOP_DEFAULT_PASSWORD", true
default:
return "", "", false
}
+209
View File
@@ -0,0 +1,209 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/mattn/go-shellwords"
)
func AIBenchmarkSources(kind, selected string) ([]dto.AIBenchmarkSource, error) {
if kind != "vllm" && kind != "ollama" {
return nil, errors.New("unsupported benchmark source")
}
selectedID := strings.SplitN(selected, ":", 2)[0]
type instance struct {
id uint
name, container string
port int
}
instances := []instance{}
apps, err := appRepo.GetBy(appRepo.WithKey(kind))
if err != nil {
return nil, err
}
for _, app := range apps {
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(app.ID))
if err != nil {
return nil, err
}
for _, install := range installs {
if selected != "" && strconv.FormatUint(uint64(install.ID), 10) != selectedID {
continue
}
instances = append(instances, instance{id: install.ID, name: install.Name, container: install.ContainerName, port: install.HttpPort})
}
}
results := []dto.AIBenchmarkSource{}
if len(instances) == 0 {
return results, nil
}
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer client.Close()
if !strings.HasPrefix(client.DaemonHost(), "unix://") {
return nil, errors.New("benchmark shortcuts require a Docker daemon on the main node; enter the remote service address manually")
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
for _, instance := range instances {
base := dto.AIBenchmarkSource{Kind: kind, Name: instance.name, Endpoint: "/v1/chat/completions", Models: []string{}, Status: "unknown"}
inspected, err := client.ContainerInspect(ctx, instance.container)
if err != nil {
base.ID = strconv.FormatUint(uint64(instance.id), 10)
base.Message = "container inspection unavailable"
results = append(results, base)
continue
}
if inspected.State != nil {
base.Status = inspected.State.Status
}
args := []string{}
if inspected.Config != nil {
args = append(args, inspected.Config.Entrypoint...)
args = append(args, inspected.Config.Cmd...)
}
if len(args) >= 3 && (args[0] == "sh" || args[0] == "bash" || strings.HasSuffix(args[0], "/sh") || strings.HasSuffix(args[0], "/bash")) && args[1] == "-c" {
parser := shellwords.NewParser()
parser.ParseEnv = false
parser.ParseBacktick = false
if parsed, err := parser.Parse(args[2]); err == nil {
args = parsed
}
}
flags := map[string]string{}
for i, arg := range args {
if key, value, ok := strings.Cut(arg, "="); ok && strings.HasPrefix(key, "--") {
flags[key] = value
} else if strings.HasPrefix(arg, "--") && i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") {
flags[arg] = args[i+1]
}
}
if name := flags["--served-model-name"]; name != "" {
base.Models = []string{name}
}
tokenizer := flags["--tokenizer"]
if tokenizer == "" {
tokenizer = flags["--model"]
for i, arg := range args {
if tokenizer == "" && arg == "serve" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
tokenizer = args[i+1]
}
}
}
if kind != "ollama" && tokenizer != "" {
if !filepath.IsAbs(tokenizer) {
base.Tokenizer = tokenizer
} else {
longest := 0
for _, mount := range inspected.Mounts {
relative, err := filepath.Rel(mount.Destination, tokenizer)
if err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && len(mount.Destination) > longest {
candidate := filepath.Join(mount.Source, relative)
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
base.Tokenizer = candidate
longest = len(mount.Destination)
}
}
}
}
}
ports := map[int]string{}
if instance.port > 0 {
ports[instance.port] = "127.0.0.1"
}
if inspected.NetworkSettings != nil {
for _, bindings := range inspected.NetworkSettings.Ports {
for _, binding := range bindings {
port, _ := strconv.Atoi(binding.HostPort)
if port > 0 {
host := binding.HostIP
if host == "" || host == "0.0.0.0" || host == "::" {
host = "127.0.0.1"
}
ports[port] = host
}
}
}
}
if len(ports) == 0 && inspected.HostConfig != nil && inspected.HostConfig.NetworkMode == "host" {
port, _ := strconv.Atoi(flags["--port"])
if port > 0 {
ports[port] = "127.0.0.1"
}
}
ordered := []int{}
for port := range ports {
ordered = append(ordered, port)
}
sort.Ints(ordered)
if len(ordered) == 0 {
ordered = append(ordered, 0)
}
for _, port := range ordered {
source := base
source.ID = fmt.Sprintf("%d:%d", instance.id, port)
if selected != "" && source.ID != selected {
continue
}
if port > 0 {
source.BaseURL = "http://" + net.JoinHostPort(ports[port], strconv.Itoa(port))
}
if selected == source.ID && source.BaseURL != "" && source.Status == "running" {
endpoint := "/v1/models"
if kind == "ollama" {
endpoint = "/api/tags"
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, source.BaseURL+endpoint, nil)
if err != nil {
return nil, err
}
response, err := (&http.Client{Timeout: 3 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(request)
if err != nil {
source.Message = "model list unavailable; enter the served model name"
} else {
raw, readErr := io.ReadAll(io.LimitReader(response.Body, 2*1024*1024))
response.Body.Close()
if response.StatusCode == http.StatusOK && readErr == nil {
var data struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
Models []struct {
Name string `json:"name"`
} `json:"models"`
}
if err := json.Unmarshal(raw, &data); err == nil {
source.Models = []string{}
for _, model := range data.Data {
source.Models = append(source.Models, model.ID)
}
for _, model := range data.Models {
source.Models = append(source.Models, model.Name)
}
}
} else {
source.Message = fmt.Sprintf("model list HTTP %d; provide model name and credentials if required", response.StatusCode)
}
}
}
results = append(results, source)
}
}
return results, nil
}
+63
View File
@@ -0,0 +1,63 @@
package service
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/files"
)
func prepareAIGatewayAppRecover(install *model.AppInstall, recoverFile, secret string) (string, func(), error) {
noop := func() {}
if !isAIGatewayApp(install.App.Key) {
return "", noop, nil
}
tmpDir, err := os.MkdirTemp(global.Dir.TmpDir, "ai-gateway-recover-")
if err != nil {
return "", noop, err
}
cleanup := func() { _ = os.RemoveAll(tmpDir) }
fail := func(err error) (string, func(), error) {
cleanup()
return "", noop, err
}
if err := files.NewFileOp().TarGzExtractPro(recoverFile, tmpDir, secret); err != nil {
return fail(err)
}
backupDir := filepath.Join(tmpDir, strings.ReplaceAll(filepath.Base(recoverFile), ".tar.gz", ""))
for _, name := range []string{"app.json", "app.tar.gz"} {
info, err := os.Lstat(filepath.Join(backupDir, name))
if err != nil || !info.Mode().IsRegular() {
return fail(buserr.New("AppBackupFileIncomplete"))
}
}
metadata, err := os.ReadFile(filepath.Join(backupDir, "app.json"))
if err != nil {
return fail(err)
}
var backup model.AppInstall
if err := json.Unmarshal(metadata, &backup); err != nil {
return fail(fmt.Errorf("unmarshal app.json failed: %w", err))
}
if backup.App.Key != install.App.Key || backup.Name != install.Name {
return fail(buserr.New("AppAttributesNotMatch"))
}
// Historical backups already include Version. Unsuffixed releases are
// SQLite; no new metadata field is required. Missing versions are invalid,
// not evidence that an archive contains SQLite data.
if strings.TrimSpace(backup.Version) == "" || strings.TrimSpace(install.Version) == "" {
return fail(buserr.New("AppBackupFileIncomplete"))
}
// Deliberately do not compare version order: restoring an older version of
// the same database type is required for failed-upgrade rollbacks.
if !isAIGatewayUpgradeVersionAllowed(install.App.Key, install.Version, backup.Version) {
return fail(buserr.New("ErrAIGatewayRecoverDatabaseTypeMismatch"))
}
return backupDir, cleanup, nil
}
+37
View File
@@ -0,0 +1,37 @@
package service
import (
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/common"
)
const aiGatewayAppKey = "ai-gateway"
func isAIGatewayApp(appKey string) bool {
return appKey == aiGatewayAppKey || appKey == "local"+aiGatewayAppKey
}
func isAIGatewayPostgresVersion(version string) bool {
return strings.HasSuffix(version, "-pg") || strings.HasSuffix(version, "-postgres")
}
// Database variants are deployment choices, not interchangeable upgrades.
func isAIGatewayUpgradeVersionAllowed(appKey, currentVersion, targetVersion string) bool {
return !isAIGatewayApp(appKey) ||
isAIGatewayPostgresVersion(currentVersion) == isAIGatewayPostgresVersion(targetVersion)
}
func sortAIGatewayVersions(versions []string) {
sort.SliceStable(versions, func(i, j int) bool {
if common.CompareVersion(versions[i], versions[j]) {
return true
}
if common.CompareVersion(versions[j], versions[i]) {
return false
}
// Prefer SQLite only when the numeric versions are equal.
return !isAIGatewayPostgresVersion(versions[i]) && isAIGatewayPostgresVersion(versions[j])
})
}
+135 -135
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{}
@@ -78,10 +74,10 @@ type IAlertService interface {
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
DeleteAlertConfig(id uint) error
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error)
DeleteAlertConfig(id uint) (string, error)
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error)
}
func NewIAlertService() IAlertService {
@@ -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
@@ -677,25 +623,26 @@ func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operato
})
}
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error) {
if err := validateAlertConfigStatus(req.Status); err != nil {
return err
return "", err
}
config, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return err
return "", err
}
name := alertConfigLogName(config.Config, config.Title)
if req.Status == constant.AlertEnable {
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
return err
return name, err
}
if config.Type == constant.Custom {
if _, err := alertwebhook.Resolve(config); err != nil {
return err
return name, err
}
}
}
return alertRepo.UpdateAlertConfig(map[string]interface{}{
return name, alertRepo.UpdateAlertConfig(map[string]interface{}{
"status": req.Status,
"update_user": operator,
}, repo.WithByID(req.ID))
@@ -867,19 +814,20 @@ func alertConfigSMSPhone(configData string) string {
return strings.TrimSpace(cfg.Phone)
}
func (a AlertService) DeleteAlertConfig(id uint) error {
_, err := alertRepo.GetConfigById(id)
func (a AlertService) DeleteAlertConfig(id uint) (string, error) {
config, err := alertRepo.GetConfigById(id)
if err != nil {
return err
return "", err
}
name := alertConfigLogName(config.Config, config.Title)
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
if err != nil {
return err
return name, err
}
if len(usedAlerts) > 0 {
return buserr.New("ErrAlertConfigInUse")
return name, buserr.New("ErrAlertConfigInUse")
}
return alertRepo.DeleteAlertConfig(repo.WithByID(id))
return name, alertRepo.DeleteAlertConfig(repo.WithByID(id))
}
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
@@ -905,7 +853,7 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
msg := email.EmailMessage{
Subject: i18n.GetMsgByKey("TestAlertTitle"),
Body: i18n.GetMsgByKey("TestAlert"),
Body: i18n.GetMsgByKey("TestAlertMessage"),
IsHTML: false,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
@@ -953,7 +901,7 @@ func resolveEmailTestConfig(req dto.AlertConfigTest) (dto.AlertEmailConfig, erro
return emailConfig, nil
}
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error) {
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error) {
if req.Type != constant.Custom {
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
}
@@ -982,12 +930,15 @@ func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertC
}
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
if !ok {
if global.LOG != nil {
global.LOG.Warnf("webhook test failed: %v", providers.ErrCustomWebhookUnsupported)
}
return dto.AlertConfigTestResult{
Success: false,
Message: providers.ErrCustomWebhookUnsupported.Error(),
}, nil
}
return tester.TestCustomWebhook(resolved)
return tester.TestCustomWebhook(resolved, lang)
}
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
@@ -1021,6 +972,23 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
alertRepo.WithByType(updateAlert.Type),
alertRepo.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 +1002,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 +1026,32 @@ 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
}
func alertConfigLogName(config, title string) string {
var value struct {
DisplayName string `json:"displayName"`
}
if json.Unmarshal([]byte(config), &value) == nil && strings.TrimSpace(value.DisplayName) != "" {
return strings.TrimSpace(value.DisplayName)
}
return title
}
+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) {
+42 -16
View File
@@ -258,26 +258,35 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
fileOp := files.NewFileOp()
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
if !fileOp.Stat(versionPath) || detail.Update {
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
return appDetailDTO, err
}
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
return appDetailDTO, err
}
if err == nil && app.Resource == constant.AppResourceRemote {
detail.Update = false
appDetailDTO.Update = false
}
switch app.Type {
case constant.RuntimePHP:
paramsPath := filepath.Join(versionPath, "data.yml")
if !fileOp.Stat(paramsPath) {
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
}
param, err := fileOp.GetContent(paramsPath)
if err != nil {
return appDetailDTO, err
}
paramMap := make(map[string]interface{})
if err = yaml.Unmarshal(param, &paramMap); err != nil {
return appDetailDTO, err
if app.Resource == constant.AppResourceRemote {
if err = json.Unmarshal([]byte(detail.Params), &paramMap); err != nil {
return appDetailDTO, err
}
appDetailDTO.Params = paramMap
} else {
paramsPath := filepath.Join(versionPath, "data.yml")
if !fileOp.Stat(paramsPath) {
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
}
param, err := fileOp.GetContent(paramsPath)
if err != nil {
return appDetailDTO, err
}
if err = yaml.Unmarshal(param, &paramMap); err != nil {
return appDetailDTO, err
}
appDetailDTO.Params = paramMap["additionalProperties"]
}
appDetailDTO.Params = paramMap["additionalProperties"]
composePath := filepath.Join(versionPath, "docker-compose.yml")
if !fileOp.Stat(composePath) {
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
@@ -308,6 +317,12 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
if appDetailDTO.DockerCompose == "" {
filename := filepath.Base(appDetailDTO.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
if app.Resource == constant.AppResourceRemote {
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
if err != nil {
return appDetailDTO, err
}
}
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
@@ -434,6 +449,12 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
} else {
if appDetail.DockerCompose == "" {
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
if app.Resource == constant.AppResourceRemote {
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
if err != nil {
return
}
}
var statusCode int
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
@@ -550,6 +571,11 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
if err != nil {
appInstall.Status = constant.StatusInstallErr
appInstall.Message = err.Error()
if saveErr := appInstallRepo.Save(context.Background(), appInstall); saveErr != nil {
err = fmt.Errorf("%w; save failed install status: %v", err, saveErr)
}
return
}
@@ -557,7 +583,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
return
}
installApp := func(t *task.Task) error {
installApp := func(t *task.Task) (err error) {
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
return err
}
+49 -7
View File
@@ -339,6 +339,11 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
if err != nil {
install.Status = constant.StatusUpErr
install.Message = err.Error()
if saveErr := appInstallRepo.Save(context.Background(), &install); saveErr != nil {
return fmt.Errorf("%w; save failed operation status: %v", err, saveErr)
}
return err
}
operation := string(req.Operate)
@@ -567,11 +572,15 @@ func (a *AppInstallService) SyncAll(systemInit bool) error {
return err
}
for _, i := range allList {
if i.Status == constant.StatusInstalling || i.Status == constant.StatusUpgrading || i.Status == constant.StatusRebuilding || i.Status == constant.StatusUninstalling {
if appInstallOperationInterruptedOnRestart(i.Status) {
if systemInit {
i.Status = constant.StatusError
i.Message = "1Panel restart causes the task to terminate"
_ = appInstallRepo.Save(context.Background(), &i)
i.Status = appInstallOperationFailureStatus(i.Status)
i.Message = constant.InterruptedMsg
if err := appInstallRepo.Save(context.Background(), &i); err != nil {
return err
}
} else if err := syncAppInstallStatus(&i, false); err != nil {
global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
}
continue
}
@@ -678,14 +687,17 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
if !canAccessVllmVersion(app.Key, detail.Version) {
continue
}
if !isAIGatewayUpgradeVersionAllowed(app.Key, install.Version, detail.Version) {
continue
}
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
if len(ignores) > 0 {
continue
}
if common.IsCrossVersion(install.Version, detail.Version) && !app.CrossVersionUpdate {
if isCrossAppVersion(app.Key, install.Version, detail.Version) && !app.CrossVersionUpdate {
continue
}
canUpgrade := common.CompareVersion(detail.Version, install.Version)
canUpgrade := isNewerAppVersion(app.Key, detail.Version, install.Version)
if app.Key == vllmAppKeyForUpgrade {
currentImage := loadVllmImageFromEnv(install.Env)
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
@@ -695,6 +707,12 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
filename := filepath.Base(detail.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
if app.Resource == constant.AppResourceRemote {
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
if err != nil {
return versions, err
}
}
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
return versions, buserr.New("ErrAppVersionUnavailable")
@@ -726,7 +744,7 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
}
}
sort.Slice(versions, func(i, j int) bool {
return common.CompareVersion(versions[i].Version, versions[j].Version)
return appVersionSortBefore(app.Key, versions[i].Version, versions[j].Version)
})
return versions, nil
}
@@ -931,10 +949,34 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
}
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
operation := ""
switch appInstall.Status {
case constant.StatusInstalling:
operation = task.TaskInstall
case constant.StatusUpgrading:
operation = task.TaskUpgrade
case constant.StatusRebuilding:
operation = task.TaskBuild
case constant.StatusUninstalling:
operation = task.TaskUninstall
case constant.StatusStarting, constant.StatusWaiting:
operation = task.TaskUpdate
case constant.StatusRestarting:
operation = task.TaskRestart
}
if operation != "" {
if err := reconcileAppInstallTaskFailure(appInstall, operation, global.DB, global.TaskDB); err != nil {
return err
}
}
switch appInstall.Status {
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
return nil
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
if !force {
return nil
}
}
cli, err := docker.NewClient()
if err != nil {
+76
View File
@@ -0,0 +1,76 @@
package service
import (
"errors"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"gorm.io/gorm"
)
func appInstallOperationFailureStatus(status string) string {
switch status {
case constant.StatusInstalling:
return constant.StatusInstallErr
case constant.StatusUpgrading:
return constant.StatusUpgradeErr
case constant.StatusUninstalling:
return constant.StatusError
default:
return constant.StatusUpErr
}
}
func appInstallOperationPending(status string) bool {
switch status {
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
return true
default:
return false
}
}
func appInstallOperationInterruptedOnRestart(status string) bool {
// ReStarting can also describe a container's Docker restart policy.
return appInstallOperationPending(status) && status != constant.StatusRestarting
}
func reconcileAppInstallTaskFailure(install *model.AppInstall, operation string, appDB, taskDB *gorm.DB) error {
if !appInstallOperationPending(install.Status) {
return nil
}
var operationTask model.Task
err := taskDB.Where("type = ? AND resource_id = ? AND operate = ?", "App", install.ID, operation).
Order("created_at DESC").First(&operationTask).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
if operationTask.Status != constant.StatusFailed && operationTask.Status != constant.StatusCanceled {
return nil
}
// A previous operation must not turn a newly queued retry into a failure.
if operationTask.CreatedAt.Before(install.UpdatedAt) &&
(operationTask.EndAt.IsZero() || operationTask.EndAt.Before(install.UpdatedAt)) {
return nil
}
message := operationTask.ErrorMsg
if message == "" {
message = "the application operation task failed"
}
status := appInstallOperationFailureStatus(install.Status)
result := appDB.Model(&model.AppInstall{}).
Where("id = ? AND status = ? AND updated_at = ?", install.ID, install.Status, install.UpdatedAt).
Updates(map[string]interface{}{"status": status, "message": message})
if result.Error != nil {
return result.Error
}
if result.RowsAffected > 0 {
install.Status = status
install.Message = message
}
return nil
}
@@ -0,0 +1,147 @@
package service
import (
"path/filepath"
"testing"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger"
)
func operationStatusTestDB(t *testing.T) (*gorm.DB, *gorm.DB, model.AppInstall) {
t.Helper()
open := func(name string, schema interface{}) *gorm.DB {
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), name)), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(schema); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
sqlDB, _ := db.DB()
if sqlDB != nil {
_ = sqlDB.Close()
}
})
return db
}
appDB := open("apps.db", &model.AppInstall{})
taskDB := open("tasks.db", &model.Task{})
install := model.AppInstall{
Name: "gb10-ds-vision", AppId: 1, AppDetailId: 1,
Version: "nvidia-gb10-dspark-0.1.1", ContainerName: "vllm-dspark-1", ServiceName: "vllm-dspark",
Status: constant.StatusStarting, Env: `{"MODEL_DIR":"/models/vision"}`,
}
if err := appDB.Omit(clause.Associations).Create(&install).Error; err != nil {
t.Fatal(err)
}
return appDB, taskDB, install
}
func TestAppInstallFailedTaskRecoversPendingState(t *testing.T) {
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusRestarting, constant.StatusInstalling} {
t.Run(state, func(t *testing.T) {
appDB, taskDB, install := operationStatusTestDB(t)
install.Status = state
if err := appDB.Omit(clause.Associations).Save(&install).Error; err != nil {
t.Fatal(err)
}
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
Status: constant.StatusFailed, ErrorMsg: "model startup script failed", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
if err := taskDB.Create(&task).Error; err != nil {
t.Fatal(err)
}
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
t.Fatal(err)
}
want := constant.StatusUpErr
if state == constant.StatusInstalling {
want = constant.StatusInstallErr
}
var stored model.AppInstall
if err := appDB.First(&stored, install.ID).Error; err != nil {
t.Fatal(err)
}
if install.Status != want || stored.Status != want || stored.Message != task.ErrorMsg || stored.Env != install.Env {
t.Fatalf("recovered state = %s/%s, message %q, env %q", install.Status, stored.Status, stored.Message, stored.Env)
}
})
}
}
func TestAppInstallPendingStateKeepsActiveAndUnrelatedTasks(t *testing.T) {
for _, taskStatus := range []string{constant.StatusExecuting, constant.StatusSuccess, "missing", "previous-failure", "other-resource"} {
t.Run(taskStatus, func(t *testing.T) {
appDB, taskDB, install := operationStatusTestDB(t)
task := model.Task{ID: "task", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
Status: taskStatus, CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
if taskStatus == "previous-failure" {
task.Status = constant.StatusFailed
task.CreatedAt = install.UpdatedAt.Add(-2 * time.Hour)
task.EndAt = install.UpdatedAt.Add(-time.Hour)
}
if taskStatus == "other-resource" {
task.Status = constant.StatusFailed
task.ResourceID++
}
if taskStatus != "missing" {
if err := taskDB.Create(&task).Error; err != nil {
t.Fatal(err)
}
}
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
t.Fatal(err)
}
if install.Status != constant.StatusStarting {
t.Fatalf("unrelated or active task changed pending state to %s", install.Status)
}
})
}
}
func TestAppInstallRecoveryDoesNotOverwriteConcurrentRetry(t *testing.T) {
appDB, taskDB, install := operationStatusTestDB(t)
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
Status: constant.StatusCanceled, ErrorMsg: "interrupted", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
if err := taskDB.Create(&task).Error; err != nil {
t.Fatal(err)
}
if err := appDB.Callback().Update().Before("gorm:update").Register("new-operation", func(db *gorm.DB) {
if err := db.Exec("UPDATE app_installs SET updated_at = ?, message = ? WHERE id = ?", install.UpdatedAt.Add(time.Hour), "new attempt", install.ID).Error; err != nil {
t.Error(err)
}
}); err != nil {
t.Fatal(err)
}
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
t.Fatal(err)
}
var stored model.AppInstall
if err := appDB.First(&stored, install.ID).Error; err != nil {
t.Fatal(err)
}
if stored.Status != constant.StatusStarting || stored.Message != "new attempt" {
t.Fatalf("recovery overwrote a newer operation: %s, %s", stored.Status, stored.Message)
}
}
func TestAppInstallRestartRecoveryCoversLifecycleStates(t *testing.T) {
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusInstalling} {
if !appInstallOperationInterruptedOnRestart(state) {
t.Errorf("restart recovery omitted %s", state)
}
}
for _, state := range []string{constant.StatusRunning, constant.StatusStopped, constant.StatusRestarting} {
if appInstallOperationInterruptedOnRestart(state) {
t.Errorf("restart recovery changes a Docker runtime state: %s", state)
}
}
}
+5 -1
View File
@@ -191,7 +191,11 @@ func (c *appSyncContext) processOneApp(item appWorkItem) appWorkResult {
if _, ok := InitTypes[app.Type]; ok {
dockerComposeUrl := fmt.Sprintf("%s/%s", versionUrl, "docker-compose.yml")
_, composeRes, err := req_helper.HandleRequestWithClient(&c.httpClient, dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
dockerComposeUrl, err := appDownloadURL(dockerComposeUrl, strconv.Itoa(c.list.LastModified))
var composeRes []byte
if err == nil {
_, composeRes, err = req_helper.HandleRequestWithClient(&c.httpClient, dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
}
if err == nil {
detail.DockerCompose = string(composeRes)
} else {
+8
View File
@@ -106,6 +106,14 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil {
return err
}
if !isAIGatewayUpgradeVersionAllowed(install.App.Key, install.Version, detail.Version) {
return buserr.New("ErrAIGatewayDatabaseTypeMismatch")
}
if isHermesApp(install.App.Key) &&
(!isNewerAppVersion(install.App.Key, detail.Version, install.Version) ||
(isCrossAppVersion(install.App.Key, install.Version, detail.Version) && !install.App.CrossVersionUpdate)) {
return buserr.New("ErrHermesUpgradeVersion")
}
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
return err
}
+64 -21
View File
@@ -8,6 +8,7 @@ import (
"maps"
"math"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
@@ -891,16 +892,52 @@ func handleMap(params map[string]interface{}, envParams map[string]string) {
}
}
func appDownloadURL(rawURL, version string) (string, error) {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return "", err
}
query := parsedURL.Query()
query.Set("v", version)
parsedURL.RawQuery = query.Encode()
return parsedURL.String(), nil
}
func currentAppDownloadURL(rawURL string) (string, error) {
storeVersion, err := settingRepo.GetValueByKey("AppStoreLastModified")
if err != nil {
return "", err
}
return appDownloadURL(rawURL, storeVersion)
}
func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, logger *logrus.Logger) (err error) {
if app.IsLocalApp() || app.IsCustomApp() {
return nil
}
defer func() {
if err != nil && appInstall != nil {
appInstall.Status = constant.StatusDownloadErr
appInstall.Message = err.Error()
}
}()
appResourceDir := path.Join(global.Dir.AppResourceDir, app.Resource)
appDownloadDir := app.GetAppResourcePath()
appVersionDir := path.Join(appDownloadDir, appDetail.Version)
fileOp := files.NewFileOp()
storeVersion, err := settingRepo.GetValueByKey("AppStoreLastModified")
if err != nil {
return err
}
cacheVersionPath := path.Join(appVersionDir, ".1panel-appstore-version")
if !appDetail.Update && fileOp.Stat(appVersionDir) {
return
if cachedVersion, readErr := os.ReadFile(cacheVersionPath); readErr == nil && string(cachedVersion) == storeVersion {
return nil
}
}
downloadURL, err := appDownloadURL(appDetail.DownloadUrl, storeVersion)
if err != nil {
return err
}
if !fileOp.Stat(appDownloadDir) {
_ = fileOp.CreateDir(appDownloadDir, constant.DirPerm)
@@ -909,23 +946,14 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
_ = fileOp.CreateDir(appVersionDir, constant.DirPerm)
}
if logger == nil {
global.LOG.Infof("download app [%s] from %s", app.Name, appDetail.DownloadUrl)
global.LOG.Infof("download app [%s] from %s", app.Name, downloadURL)
} else {
logger.Printf("download app [%s] from %s", app.Name, appDetail.DownloadUrl)
logger.Printf("download app [%s] from %s", app.Name, downloadURL)
}
filePath := path.Join(appVersionDir, app.Key+"-"+appDetail.Version+".tar.gz")
defer func() {
if err != nil {
if appInstall != nil {
appInstall.Status = constant.StatusDownloadErr
appInstall.Message = err.Error()
}
}
}()
if err = files.DownloadFileWithProxy(appDetail.DownloadUrl, filePath); err != nil {
if err = files.DownloadFileWithProxy(downloadURL, filePath); err != nil {
if logger == nil {
global.LOG.Errorf("download app [%s] error %v", app.Name, err)
} else {
@@ -942,6 +970,9 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
return
}
_ = fileOp.DeleteFile(filePath)
if err = os.WriteFile(cacheVersionPath, []byte(storeVersion), constant.FilePerm); err != nil {
return err
}
appDetail.Update = false
_ = appDetailRepo.Update(context.Background(), appDetail)
return
@@ -1406,9 +1437,8 @@ func handleErr(install model.AppInstall, err error, out string) error {
}
func doNotNeedSync(installed model.AppInstall) bool {
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading ||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr ||
installed.Status == constant.StatusStarting || installed.Status == constant.StatusRestarting || installed.Status == constant.StatusWaiting
return appInstallOperationPending(installed.Status) || installed.Status == constant.StatusSyncing ||
installed.Status == constant.StatusInstallErr || installed.Status == constant.StatusUpgradeErr || installed.Status == constant.StatusUpErr
}
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
@@ -1502,6 +1532,11 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
}
for _, installed := range appInstallList {
if sync && appInstallOperationPending(installed.Status) {
if err := syncAppInstallStatus(&installed, false); err != nil {
return nil, err
}
}
if updated && ignoreUpdate(installed) {
continue
}
@@ -1567,11 +1602,14 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
var versions []string
for _, appDetail := range details {
if !isAIGatewayUpgradeVersionAllowed(installed.App.Key, installed.Version, appDetail.Version) {
continue
}
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(appDetail.ID), appIgnoreUpgradeRepo.WithScope("version"))
if len(ignores) > 0 {
continue
}
if common.IsCrossVersion(installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate {
if isCrossAppVersion(installed.App.Key, installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate {
continue
}
versions = append(versions, appDetail.Version)
@@ -1586,7 +1624,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
continue
}
versions = common.GetSortedVersions(versions)
versions = sortAppVersions(installed.App.Key, versions)
lastVersion := versions[0]
if installed.App.Key == constant.AppMysql || installed.App.Key == constant.AppMysqlCluster {
@@ -1601,10 +1639,10 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
}
}
if common.IsCrossVersion(installed.Version, lastVersion) {
if isCrossAppVersion(installed.App.Key, installed.Version, lastVersion) {
installDTO.CanUpdate = installed.App.CrossVersionUpdate
} else {
installDTO.CanUpdate = common.CompareVersion(lastVersion, installed.Version)
installDTO.CanUpdate = isNewerAppVersion(installed.App.Key, lastVersion, installed.Version)
}
if updated {
@@ -2275,7 +2313,12 @@ func getAppVersions(key string, details []model.AppDetail) []string {
}
versionsRaw = append(versionsRaw, detail.Version)
}
versions = common.GetSortedVersions(versionsRaw)
if isAIGatewayApp(key) {
sortAIGatewayVersions(versionsRaw)
versions = versionsRaw
} else {
versions = sortAppVersions(key, versionsRaw)
}
if hasLatest {
versions = append([]string{latestVersion}, versions...)
}
+24 -8
View File
@@ -132,11 +132,9 @@ func (u *BackupService) AppRecover(req dto.CommonRecover) error {
if !fileOp.Stat(req.File) {
return buserr.WithName("ErrFileNotFound", req.File)
}
if _, err := compose.Down(install.GetComposePath()); err != nil {
return err
}
if err := handleAppRecover(&install, nil, req.File, false, req.Secret, req.TaskID); err != nil {
global.LOG.Errorf("recover app %s failed, err: %v", req.DetailName, err)
return err
}
return nil
}
@@ -200,8 +198,20 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
}
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error {
// Validate AI Gateway backups before stopping the app, creating rollback
// backups, or touching its files/database. Keep the validated extraction for
// the restore so uploaded/encrypted archives are not unpacked twice.
preparedPath, cleanup, err := prepareAIGatewayAppRecover(install, recoverFile, secret)
if err != nil {
return err
}
if parentTask == nil && !isRollback {
if _, err := compose.Down(install.GetComposePath()); err != nil {
cleanup()
return err
}
}
var (
err error
recoverTask *task.Task
isOk = false
rollbackFile string
@@ -210,11 +220,13 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
if parentTask == nil {
recoverTask, err = task.NewTaskWithOps(install.Name, task.TaskRecover, task.TaskScopeBackup, taskID, install.ID)
if err != nil {
cleanup()
return err
}
}
recoverApp := func(t *task.Task) error {
defer cleanup()
fileOp := files.NewFileOp()
if !isRollback {
rollbackFile = path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)))
@@ -223,10 +235,13 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
}
}
if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil {
return err
tmpPath := preparedPath
if tmpPath == "" {
if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil {
return err
}
tmpPath = strings.ReplaceAll(recoverFile, ".tar.gz", "")
}
tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "")
if err := fileOp.TarGzExtractPro(tmpPath+"/app.tar.gz", tmpPath, ""); err != nil {
return err
}
@@ -236,7 +251,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
} else {
_, _ = compose.Up(install.GetComposePath())
}
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
_ = os.RemoveAll(tmpPath)
}()
if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") {
@@ -397,6 +412,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
}
recoverTask.AddSubTask(task.GetTaskName(install.Name, task.TaskRecover, task.TaskScopeBackup), recoverApp, rollBackApp)
go func() {
defer cleanup()
_ = recoverTask.Execute()
}()
return nil
+84 -48
View File
@@ -371,43 +371,39 @@ func loadComposePathAndFiles(composeName string, dockerClient *client.Client) (s
}
config := targetContainer.Labels[composeConfigLabel]
workdir := targetContainer.Labels[composeWorkdirLabel]
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
composePath = config
} else {
composePath = workdir
files, err := resolveComposeFiles(workdir, config)
if err != nil {
return "", nil, err
}
composePath = strings.Join(files, ",")
}
composeFiles := normalizeComposeFiles(composePath)
if len(composeFiles) == 0 {
return "", nil, fmt.Errorf("compose file not found for %s", composeName)
composeFiles, err := normalizeComposeFiles(composePath)
if err != nil {
return "", nil, err
}
return composePath, composeFiles, nil
return strings.Join(composeFiles, ","), composeFiles, nil
}
func normalizeComposeFiles(composePath string) []string {
items := strings.Split(composePath, ",")
result := make([]string, 0)
seen := make(map[string]struct{})
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
stat, err := os.Stat(item)
if err == nil && stat.IsDir() {
item = path.Join(item, "docker-compose.yml")
}
if _, err := os.Stat(item); err != nil {
continue
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
result = append(result, item)
func normalizeComposeFiles(composePath string) ([]string, error) {
files, err := resolveComposeFiles("", composePath)
if err != nil {
return nil, err
}
sort.Strings(result)
return result
for i, file := range files {
info, err := os.Stat(file)
if err == nil && info.IsDir() {
file = filepath.Join(file, "docker-compose.yml")
info, err = os.Stat(file)
files[i] = file
}
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() {
return nil, fmt.Errorf("compose path %s is not a regular file", file)
}
}
return files, nil
}
func (c *composeBackupContext) close() {
@@ -621,20 +617,46 @@ func safeJoinWithinBase(baseDir, name string) (string, error) {
}
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
if recoverCtx.targetDir != "" {
_ = os.RemoveAll(recoverCtx.targetDir)
composeFiles := recoverCtx.meta.Files
if strings.TrimSpace(recoverCtx.meta.ComposePath) != "" {
filesByPath := make(map[string]composeBackupFile, len(composeFiles))
for _, item := range composeFiles {
filesByPath[filepath.Clean(item.OriginalPath)] = item
}
orderedFiles := make([]composeBackupFile, 0, len(composeFiles))
for _, file := range strings.Split(recoverCtx.meta.ComposePath, ",") {
file = strings.TrimSpace(file)
if file == "" {
continue
}
file = filepath.Clean(file)
item, ok := filesByPath[file]
if !ok {
if len(composeFiles) != 1 || file != filepath.Dir(composeFiles[0].OriginalPath) {
return fmt.Errorf("compose file %s is missing from backup metadata", file)
}
item = composeFiles[0]
}
orderedFiles = append(orderedFiles, item)
}
composeFiles = orderedFiles
}
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
return err
if len(composeFiles) == 0 {
return fmt.Errorf("compose files are missing from backup metadata")
}
restored := make([]string, 0, len(recoverCtx.meta.Files))
for _, item := range recoverCtx.meta.Files {
restored := make([]string, 0, len(composeFiles))
sourceByTarget := make(map[string]string, len(composeFiles))
for _, item := range composeFiles {
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
if err != nil {
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
}
if !recoverCtx.fileOp.Stat(backupPath) {
continue
info, err := os.Stat(backupPath)
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("compose backup path %s is not a regular file", backupPath)
}
targetName := item.FileName
if item.RelativePath != "" {
@@ -650,6 +672,29 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
if err != nil {
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
}
sourcePath := item.OriginalPath
if sourcePath == "" {
sourcePath = backupPath
}
sourcePath = filepath.Clean(sourcePath)
if previous, ok := sourceByTarget[targetPath]; ok && previous != sourcePath {
return fmt.Errorf("compose files %q and %q resolve to the same restore path %q", previous, sourcePath, targetPath)
}
sourceByTarget[targetPath] = sourcePath
restored = append(restored, targetPath)
}
if recoverCtx.targetDir != "" {
_ = os.RemoveAll(recoverCtx.targetDir)
}
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
return err
}
for i, item := range composeFiles {
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
if err != nil {
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
}
targetPath := restored[i]
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
return err
}
@@ -660,7 +705,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
return err
}
restored = append(restored, targetPath)
}
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
if recoverCtx.fileOp.Stat(envPath) {
@@ -672,14 +716,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
return err
}
}
if len(restored) == 0 {
defaultPath := path.Join(recoverCtx.targetDir, "docker-compose.yml")
if !recoverCtx.fileOp.Stat(defaultPath) {
return fmt.Errorf("compose file not found in backup data")
}
restored = append(restored, defaultPath)
}
sort.Strings(restored)
recoverCtx.composePath = strings.Join(restored, ",")
return nil
}
+62 -55
View File
@@ -326,40 +326,47 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
case "container":
inspectInfo, err = client.ContainerInspect(context.Background(), req.ID)
case "compose":
filePath := ""
cli, err := docker.NewDockerClient()
if err != nil {
return "", err
}
defer cli.Close()
config, workdir := "", ""
options := container.ListOptions{All: true}
options.Filters = filters.NewArgs()
options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.ID))
containers, err := cli.ContainerList(context.Background(), options)
containers, err := client.ContainerList(context.Background(), options)
if err != nil {
return "", err
}
for _, container := range containers {
config := container.Labels[composeConfigLabel]
if len(req.Detail) != 0 && strings.Contains(config, req.Detail) {
config = req.Detail
}
workdir := container.Labels[composeWorkdirLabel]
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
filePath = config
break
} else {
filePath = workdir
break
}
}
if len(containers) == 0 {
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.ID))
filePath = composeItem.Path
composeItem, err := composeRepo.GetRecord(repo.WithByName(req.ID))
if err != nil {
return "", err
}
config = composeItem.Path
workdir = filepath.Dir(primaryComposePath(config))
} else {
config = containers[0].Labels[composeConfigLabel]
workdir = containers[0].Labels[composeWorkdirLabel]
}
if _, err := os.Stat(filePath); err != nil {
composeFiles, err := resolveComposeFiles(workdir, config)
if err != nil {
return "", err
}
filePath := composeFiles[0]
if strings.TrimSpace(req.Detail) != "" {
selectedFiles, err := resolveComposeFiles(workdir, req.Detail)
if err != nil {
return "", err
}
if len(selectedFiles) != 1 || !slices.Contains(composeFiles, selectedFiles[0]) {
return "", fmt.Errorf("compose file %s does not belong to project %s", req.Detail, req.ID)
}
filePath = selectedFiles[0]
}
info, err := os.Stat(filePath)
if err != nil {
return "", err
}
if !info.Mode().IsRegular() {
return "", fmt.Errorf("compose path %s is not a regular file", filePath)
}
content, err := os.ReadFile(filePath)
if err != nil {
return "", err
@@ -383,11 +390,6 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
}
func (u *ContainerService) Prune(req dto.ContainerPrune) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
name := ""
switch req.PruneType {
case "container":
@@ -408,6 +410,14 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
}
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
if err := t.TaskCtx.Err(); err != nil {
return err
}
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
pruneFilters := filters.NewArgs()
if req.WithTagAll {
pruneFilters.Add("dangling", "false")
@@ -431,10 +441,7 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
}
SpaceReclaimed = int(rep.SpaceReclaimed)
case "network":
_, err := client.NetworksPrune(context.Background(), pruneFilters)
if err != nil {
return err
}
return cleanUnusedNetworks(t, client)
case "volume":
versions, err := client.ServerVersion(context.Background())
if err != nil {
@@ -803,33 +810,33 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) {
messageChan := make(chan string, 1024)
errorChan := make(chan error, 1)
doneChan := make(chan struct{})
go func() {
<-ctx.Request.Context().Done()
close(doneChan)
}()
logCtx, cancel := context.WithCancel(ctx.Request.Context())
defer cancel()
doneChan := logCtx.Done()
go collectLogs(doneChan, params, messageChan, errorChan)
ctx.Stream(func(w io.Writer) bool {
select {
case msg, ok := <-messageChan:
if !ok {
return false
}
_, err := fmt.Fprintf(w, "data: %s\n\n", msg)
if err != nil {
return false
}
return true
case err := <-errorChan:
if err != nil {
_, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err.Error())
}
return false
}
return writeContainerLogMessage(w, doneChan, messageChan, errorChan)
})
}
func writeContainerLogMessage(w io.Writer, done <-chan struct{}, messages <-chan string, errors <-chan error) bool {
select {
case <-done:
return false
case message, ok := <-messages:
if ok {
_, err := fmt.Fprintf(w, "data: %s\n\n", message)
return err == nil
}
// The collector closes both channels when it finishes. Drain all buffered
// logs before handling its result, or a ready error channel can drop the tail.
if err := <-errors; err != nil {
_, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err)
}
return false
}
}
func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<- string, errorChan chan<- error) {
defer close(messageChan)
defer close(errorChan)
+71 -49
View File
@@ -71,10 +71,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
ContainerCount: 0,
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
ConfigFile: localItem.Path,
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"),
Workdir: filepath.Dir(primaryComposePath(localItem.Path)),
}
composeItemLocal.CreatedBy = "1Panel"
composeItemLocal.Path = localItem.Path
composeLocalMap[localItem.Name] = composeItemLocal
}
@@ -112,11 +111,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
if ok {
composeItem.CreatedBy = createdBy
}
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
composeItem.Path = config
} else {
composeItem.Path = workdir
}
for i := 0; i < len(composeCreatedByLocal); i++ {
if composeCreatedByLocal[i].Name == name {
composeItem.CreatedBy = "1Panel"
@@ -147,7 +141,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
for key, value := range mergedMap {
value.Name = key
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
composeFiles, err := resolveComposeFiles(value.Workdir, value.ConfigFile)
value.Path = strings.Join(composeFiles, ",")
value.ComposeFileExists = err == nil && composeFileExists(composeFiles)
value.IsPinned = pinnedByName[key]
records = append(records, value)
}
@@ -192,29 +188,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
return int64(total), listItem, nil
}
func composeFileExists(workdir, configFile string) bool {
workdir = strings.TrimSpace(workdir)
configFile = strings.TrimSpace(configFile)
if configFile == "" {
return false
}
for _, item := range strings.Split(configFile, ",") {
file := strings.TrimSpace(item)
if file == "" {
continue
}
if !filepath.IsAbs(file) && workdir != "" {
file = filepath.Join(workdir, file)
}
file = filepath.Clean(file)
info, err := os.Stat(file)
if err == nil && !info.IsDir() {
return true
}
}
return false
}
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
if err := validateComposeCreateName(req); err != nil {
return false, err
@@ -522,10 +495,6 @@ func isComposeProjectNameEmptyError(err error) bool {
}
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
if len(req.Path) == 0 && req.Operation == "delete" {
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
return nil
}
if cmd.CheckIllegal(req.Path, req.Operation) {
return buserr.New("ErrCmdIllegal")
}
@@ -535,13 +504,19 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
}
if req.WithFile {
for _, item := range strings.Split(req.Path, ",") {
if len(item) != 0 {
_ = os.RemoveAll(path.Dir(item))
item = strings.TrimSpace(item)
if !filepath.IsAbs(item) || filepath.Dir(item) == "/" {
continue
}
info, err := os.Stat(item)
if err == nil && info.Mode().IsRegular() {
if err := os.RemoveAll(filepath.Dir(item)); err != nil && !req.Force {
return err
}
}
}
}
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
return nil
return composeRepo.DeleteRecord(repo.WithByName(req.Name))
}
if req.Operation == "up" {
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
@@ -584,13 +559,13 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
write.Flush()
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.DetailPath)
if err := newComposeEnv(req.DetailPath, req.Env); err != nil {
if err := newComposeEnv(primaryComposePath(req.Path), req.Env); err != nil {
return err
}
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
if err := recreateCompose(string(oldFile), req.Path, req.Name); err != nil {
if err := recreateCompose(string(oldFile), req.Path, req.DetailPath, req.Name); err != nil {
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
}
return fmt.Errorf("update failed when handle compose up, err: %s", err)
@@ -702,9 +677,18 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
}
func removeContainerForCompose(composeName, composePath string) error {
if _, err := os.Stat(composePath); err == nil {
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil {
return errors.New(stdout)
composeFiles, err := resolveComposeFiles("", composePath)
filesAvailable := err == nil
for _, file := range composeFiles {
info, err := os.Stat(file)
if err != nil || !info.Mode().IsRegular() {
filesAvailable = false
break
}
}
if filesAvailable {
if stdout, err := compose.Operate(strings.Join(composeFiles, ","), "down", composeName); err != nil {
return fmt.Errorf("docker-compose down failed, std: %s, err: %w", stdout, err)
}
return nil
}
@@ -722,14 +706,17 @@ func removeContainerForCompose(composeName, composePath string) error {
if err != nil {
return err
}
var removeErrors []error
for _, c := range containers {
_ = client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
if err := client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}); err != nil {
removeErrors = append(removeErrors, fmt.Errorf("remove container %s failed: %w", c.ID, err))
}
}
return nil
return errors.Join(removeErrors...)
}
func recreateCompose(content, path, projectName string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
func recreateCompose(content, composePath, detailPath, projectName string) error {
file, err := os.OpenFile(detailPath, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil {
return err
}
@@ -738,7 +725,7 @@ func recreateCompose(content, path, projectName string) error {
_, _ = write.WriteString(content)
write.Flush()
if stdout, err := compose.Up(path, projectName); err != nil {
if stdout, err := compose.Up(composePath, projectName); err != nil {
return errors.New(string(stdout))
}
return nil
@@ -746,6 +733,9 @@ func recreateCompose(content, path, projectName string) error {
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
for i := 0; i < len(list); i++ {
if list[i].Path == "" {
continue
}
tmpPath := list[i].Path
if strings.Contains(list[i].Path, ",") {
tmpPath = strings.Split(list[i].Path, ",")[0]
@@ -774,3 +764,35 @@ func newComposeEnv(pathItem string, env string) error {
}
return nil
}
func resolveComposeFiles(workdir, configFile string) ([]string, error) {
workdir = strings.TrimSpace(workdir)
var files []string
for _, item := range strings.Split(configFile, ",") {
file := strings.TrimSpace(item)
if file == "" {
continue
}
if !filepath.IsAbs(file) {
if !filepath.IsAbs(workdir) {
return nil, fmt.Errorf("cannot resolve compose file %s without an absolute working directory", file)
}
file = filepath.Join(workdir, file)
}
files = append(files, filepath.Clean(file))
}
if len(files) == 0 {
return nil, fmt.Errorf("compose config file is not specified")
}
return files, nil
}
func composeFileExists(files []string) bool {
for _, file := range files {
info, err := os.Stat(file)
if err == nil && info.Mode().IsRegular() {
return true
}
}
return false
}
+119
View File
@@ -2,16 +2,24 @@ package service
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
)
var networkCleanupSlot = make(chan struct{}, 1)
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
client, err := docker.NewDockerClient()
if err != nil {
@@ -172,3 +180,114 @@ func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error {
}
return nil
}
func cleanUnusedNetworks(t *task.Task, cli *client.Client) error {
ctx := t.TaskCtx
select {
case networkCleanupSlot <- struct{}{}:
defer func() { <-networkCleanupSlot }()
case <-ctx.Done():
return ctx.Err()
}
if err := ctx.Err(); err != nil {
return err
}
networks, err := cli.NetworkList(ctx, network.ListOptions{})
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
return err
}
deleted, skipped, failed := 0, 0, 0
defer func() {
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
"deleted": deleted, "skipped": skipped, "failed": failed,
}))
}()
var used map[string]bool
sort.Slice(networks, func(i, j int) bool { return networks[i].Name < networks[j].Name })
for _, n := range networks {
if err := ctx.Err(); err != nil {
return err
}
switch n.Name {
case "none", "host", "bridge", "1panel-network":
skipped++
continue
}
if n.Scope != "local" || n.Ingress || n.ConfigOnly {
skipped++
continue
}
values := map[string]interface{}{"name": n.Name, "id": n.ID}
if used == nil {
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
return err
}
used = make(map[string]bool)
for _, c := range containers {
if c.NetworkSettings == nil {
continue
}
for name, endpoint := range c.NetworkSettings.Networks {
used[name] = true
if endpoint != nil {
used[endpoint.NetworkID] = true
}
}
}
}
if used[n.Name] || used[n.ID] {
skipped++
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
continue
}
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
if errdefs.IsNotFound(err) {
skipped++
} else {
failed++
t.Logf("Failed to inspect network [%s] (%s): %v", n.Name, n.ID, err)
}
continue
}
if len(inspected.Containers) > 0 {
skipped++
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
continue
}
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
switch {
case errdefs.IsNotFound(err):
skipped++
case errdefs.IsConflict(err):
skipped++
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
default:
failed++
t.Logf("Failed to remove network [%s] (%s): %v", n.Name, n.ID, err)
}
continue
}
deleted++
}
if err := ctx.Err(); err != nil {
return err
}
if failed > 0 {
return errors.New(i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
}
return nil
}
+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,
+105 -63
View File
@@ -43,6 +43,10 @@ type IDashboardService interface {
LoadQuickOptions() []dto.QuickJump
ChangeQuick(req dto.ChangeQuicks) error
ListQuickDirectories() ([]model.QuickDirectory, error)
CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error)
UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error)
DeleteQuickDirectory(req dto.QuickDirectoryDelete) error
LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error)
ChangeShow(req dto.SettingUpdate) error
@@ -242,7 +246,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" {
@@ -397,16 +401,15 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
}
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
showCount := 0
var quicks []model.QuickJump
quicks := make([]model.QuickJump, 0, len(req.Quicks))
for _, item := range req.Quicks {
var quick model.QuickJump
if item.IsShow {
showCount++
}
if err := copier.Copy(&quick, &item); err != nil {
return err
}
quicks = append(quicks, quick)
quicks = append(quicks, model.QuickJump{
BaseModel: model.BaseModel{ID: item.ID},
IsShow: item.IsShow,
})
}
if showCount == 0 {
return buserr.New("ErrMinQuickJump")
@@ -455,26 +458,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 +508,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 +534,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 +616,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 +628,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:
@@ -674,6 +709,13 @@ func loadQuickJump(base *dto.DashboardBase) {
quicks := launcherRepo.ListQuickJump(false)
for i := 0; i < len(quicks); i++ {
switch quicks[i].Name {
case "File":
count, err := launcherRepo.CountQuickDirectories()
if err != nil {
global.LOG.Errorf("load quick directory count: %v", err)
}
quicks[i].Detail = fmt.Sprintf("%d", count)
quicks[i].Alias = ""
case "Agent":
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
case "Website":
+63 -19
View File
@@ -8,6 +8,12 @@ import (
"os"
"os/exec"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -17,12 +23,15 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/docker/docker/api/types/container"
dockerclient "github.com/docker/docker/client"
_ "github.com/go-sql-driver/mysql"
)
type RedisService struct{}
// The CLI container is shared by all remote Redis databases on this node.
var redisCliInstallMutex sync.Mutex
type IRedisService interface {
UpdateConf(req dto.RedisConfUpdate) error
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
@@ -33,7 +42,7 @@ type IRedisService interface {
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
CheckHasCli() bool
InstallCli() error
InstallCli(req dto.RedisCliInstall) (string, error)
}
func NewIRedisService() IRedisService {
@@ -61,30 +70,65 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
}
func (u *RedisService) CheckHasCli() bool {
client, err := docker.NewDockerClient()
if err != nil {
return false
}
defer client.Close()
containerLists, err := client.ContainerList(context.Background(), container.ListOptions{})
if err != nil {
return false
}
for _, item := range containerLists {
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
return true
}
}
return false
installed, _ := u.checkCliInstalled()
return installed
}
func (u *RedisService) InstallCli() error {
func (u *RedisService) checkCliInstalled() (bool, error) {
client, err := docker.NewDockerClient()
if err != nil {
return false, err
}
defer client.Close()
info, err := client.ContainerInspect(context.Background(), "1Panel-redis-cli-tools")
if dockerclient.IsErrNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
return info.ContainerJSONBase != nil && info.State != nil && info.State.Running, nil
}
func (u *RedisService) InstallCli(req dto.RedisCliInstall) (string, error) {
if !redisCliInstallMutex.TryLock() {
return "", buserr.New("TaskIsExecuting")
}
defer redisCliInstallMutex.Unlock()
item := dto.ContainerOperate{
TaskID: req.TaskID,
Name: "1Panel-redis-cli-tools",
Image: "redis:7.4.4",
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
}
return NewIContainerService().ContainerCreate(item, false)
running, err := taskRepo.GetFirst(
repo.WithByName(task.GetTaskName(item.Name, task.TaskCreate, task.TaskScopeContainer)),
repo.WithByType(task.TaskScopeContainer),
taskRepo.WithByStatus(constant.StatusExecuting),
)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return "", err
}
if running.ID != "" {
return running.ID, nil
}
installed, err := u.checkCliInstalled()
if err != nil || installed {
return "", err
}
if item.TaskID == "" {
item.TaskID = uuid.NewString()
}
if _, err := taskRepo.GetFirst(taskRepo.WithByID(item.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) {
if err != nil {
return "", err
}
return "", buserr.New("TaskIsExecuting")
}
if err := NewIContainerService().ContainerCreate(item, true); err != nil {
return "", err
}
return item.TaskID, nil
}
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
+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
+147 -425
View File
@@ -5,71 +5,55 @@ 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"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"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 +66,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
}
@@ -95,83 +79,51 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
baseInfo.PingStatus = firewall.LoadPingStatus()
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6
for _, family := range []struct {
command string
status *dto.FirewallBackendFamilyStatus
}{
{"iptables", &baseInfo.IPv4},
{"ip6tables", &baseInfo.IPv6},
} {
policy, err := loadForwardPolicy(family.command)
if err != nil {
global.LOG.Warnf("inspect %s FORWARD policy: %v", family.command, err)
continue
}
family.status.ForwardPolicy = policy
}
return baseInfo, nil
}
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 +150,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,358 +161,128 @@ 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
}
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) {
if !cmd.Which(command) {
command += "-nft"
if !cmd.Which(command) {
return "", nil
}
}
output, err := cmd.NewCommandMgr(cmd.WithTimeout(5*time.Second)).RunWithOptionalSudoAndStdout(command, "-t", "filter", "-w", "2", "-S", "FORWARD")
if err != nil {
return "", err
}
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) == 3 && fields[0] == "-P" && fields[1] == "FORWARD" {
if fields[2] != "ACCEPT" && fields[2] != "DROP" {
return "", fmt.Errorf("unexpected FORWARD policy: %s", fields[2])
}
return fields[2], nil
}
}
return "", errors.New("FORWARD default policy was not found")
}
func lastForwardingSyncError() string {
forwardingSyncStateMu.RLock()
defer forwardingSyncStateMu.RUnlock()
if forwardingLastSyncErr == nil {
return ""
}
return forwardingLastSyncErr.Error()
}
func mergeForwardingInventory(stored []model.ForwardingRule, runtime []forwarding.Rule) ([]forwardingInventoryItem, error) {
items := make([]forwardingInventoryItem, 0, len(stored)+len(runtime))
byIdentity := make(map[string]int, len(stored)+len(runtime))
for _, record := range stored {
rule, err := forwarding.NormalizeRule(forwarding.Rule{
Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP,
TargetPort: record.TargetPort, Interface: record.Interface,
})
if err != nil {
return nil, fmt.Errorf("normalize desired forwarding rule: %w", err)
}
key := rule.Identity()
byIdentity[key] = len(items)
items = append(items, forwardingInventoryItem{ID: record.ID, Rule: rule, IsDesired: true})
}
for _, observed := range runtime {
rule, err := forwarding.NormalizeRule(observed)
if err != nil {
return nil, fmt.Errorf("normalize runtime forwarding rule: %w", err)
}
key := rule.Identity()
if index, exists := byIdentity[key]; exists {
items[index].IsRuntime = true
continue
}
byIdentity[key] = len(items)
items = append(items, forwardingInventoryItem{Rule: rule, IsRuntime: true})
}
return items, nil
}
func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool {
values := []string{
item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP,
item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(),
}
for _, value := range values {
if strings.Contains(strings.ToLower(value), keyword) {
return true
}
}
return false
}
func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate, t *task.Task) (resultErr error) {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
type operationBatch struct {
operation forwarding.OperationType
rules []forwarding.Rule
}
groups := make([]operationBatch, 0)
for _, operation := range request.Rules {
kind := forwarding.OperationType(operation.Operation)
if kind != forwarding.OperationAdd && kind != forwarding.OperationRemove {
return fmt.Errorf("unsupported forwarding operation %q", operation.Operation)
}
if len(groups) == 0 || groups[len(groups)-1].operation != kind {
groups = append(groups, operationBatch{operation: kind})
}
for _, protocol := range strings.Split(operation.Protocol, "/") {
rule, err := forwarding.NormalizeRule(forwarding.Rule{
Family: operation.Family, Protocol: protocol, Port: operation.Port,
TargetIP: operation.TargetIP, TargetPort: operation.TargetPort, Interface: operation.Interface,
})
if err != nil {
return err
}
groups[len(groups)-1].rules = append(groups[len(groups)-1].rules, rule)
}
}
stored, err := s.rules.List(ctx)
if err != nil {
return err
}
byIdentity := make(map[string]model.ForwardingRule, len(stored))
for index, rule := range forwardingRulesFromModels(stored) {
normalized, err := forwarding.NormalizeRule(rule)
if err != nil {
return err
}
byIdentity[normalized.Identity()] = stored[index]
}
succeeded, failed, skipped := 0, 0, 0
var nativeFailure error
defer func() {
recordForwardingSyncError(errors.Join(resultErr, nativeFailure))
if t != nil {
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{"succeeded": succeeded, "failed": failed}))
if skipped > 0 {
t.Logf("%s: %d", i18n.GetMsgByKey("FirewallCreateRuleSkipped"), skipped)
}
}
}()
record := func(operation forwarding.OperationType, rule forwarding.Rule, status string, cause error) {
label := fmt.Sprintf("%s %s %s %s -> %s:%s", operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort)
switch status {
case "skipped":
skipped++
if t != nil {
t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause)
}
case "failed":
failed++
if t != nil {
t.LogFailedWithErr(label, cause)
}
default:
succeeded++
if t != nil {
t.LogSuccess(label)
}
}
}
if len(request.Rules) == 2 && len(groups) == 2 && groups[0].operation == forwarding.OperationRemove && groups[1].operation == forwarding.OperationAdd {
old := make(map[string]bool, len(groups[0].rules))
for _, rule := range groups[0].rules {
old[rule.Identity()] = true
}
unchanged := len(old) == len(groups[1].rules)
duplicate := false
for _, rule := range groups[1].rules {
key := rule.Identity()
unchanged = unchanged && old[key]
if _, exists := byIdentity[key]; exists && !old[key] {
duplicate = true
}
}
if unchanged || duplicate {
for _, group := range groups {
for _, rule := range group.rules {
record(group.operation, rule, "skipped", buserr.New("ErrRecordExist"))
}
}
return nil
}
for _, rule := range groups[1].rules {
if rule.Family != forwarding.FamilyIPv6 {
continue
}
interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile)
if err != nil {
return fmt.Errorf("check IPv6 Router Advertisement: %w", err)
}
if len(interfaces) > 0 {
return fmt.Errorf("IPv6 forwarding blocked: interfaces %s may depend on RA/SLAAC with accept_ra=1; persist accept_ra=2 on interfaces that require RA before retrying", strings.Join(interfaces, ", "))
}
break
}
}
var client forwarding.Adapter
var failures []error
for _, group := range groups {
byFamily := make(map[string][]forwarding.Rule, 2)
seen := make(map[string]bool, len(group.rules))
for _, rule := range group.rules {
key := rule.Identity()
_, exists := byIdentity[key]
if seen[key] || (group.operation == forwarding.OperationAdd && exists) {
record(group.operation, rule, "skipped", buserr.New("ErrRecordExist"))
continue
}
seen[key] = true
byFamily[rule.Family] = append(byFamily[rule.Family], rule)
}
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
rules := byFamily[family]
if len(rules) == 0 {
continue
}
err := ctx.Err()
if err == nil && client == nil {
var enabled bool
enabled, err = s.forwardingEnabled()
if err == nil && !enabled {
err = fmt.Errorf("%w: forwarding is not initialized", filter.ErrProviderUnavailable)
}
if err == nil {
client, err = s.clientFactory()
}
}
if err == nil {
if group.operation == forwarding.OperationAdd {
err = client.CreateRules(ctx, rules)
} else {
err = client.DeleteRules(ctx, rules)
}
}
if err != nil {
nativeFailure = errors.Join(nativeFailure, err)
if !request.ForceDelete || !forwardingOperationsOnlyRemove(request.Rules) || ctx.Err() != nil {
failures = append(failures, err)
for _, rule := range rules {
record(group.operation, rule, "failed", err)
}
continue
}
if t != nil {
t.Logf("force delete database records: %v", err)
}
}
for start := 0; start < len(rules); start += 500 {
batch := rules[start:min(start+500, len(rules))]
records := make([]model.ForwardingRule, 0, len(batch))
ids := make([]uint, 0, len(batch))
for _, rule := range batch {
if group.operation == forwarding.OperationAdd {
records = append(records, model.ForwardingRule{Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
} else if stored, exists := byIdentity[rule.Identity()]; exists {
ids = append(ids, stored.ID)
}
}
if group.operation == forwarding.OperationAdd {
err = s.rules.CreateBatch(context.WithoutCancel(ctx), records)
} else {
err = s.rules.DeleteBatch(context.WithoutCancel(ctx), ids)
}
if err != nil {
failures = append(failures, err)
}
for index, rule := range batch {
if err != nil {
record(group.operation, rule, "failed", err)
continue
}
if group.operation == forwarding.OperationAdd {
byIdentity[rule.Identity()] = records[index]
} else {
delete(byIdentity, rule.Identity())
}
record(group.operation, rule, "succeeded", nil)
}
}
}
if group.operation == forwarding.OperationRemove && len(failures) > 0 {
return errors.Join(failures...)
}
}
return errors.Join(failures...)
}
+126
View File
@@ -0,0 +1,126 @@
package service
import (
"regexp"
"sort"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/common"
)
// Release names at https://github.com/NousResearch/hermes-agent/releases pair
// these historical calendar tags with their semantic versions. This includes
// every calendar version published in the 1Panel app store. Do not rewrite
// stored versions: backup metadata, restore and rollback retain the original tag.
var hermesHistoricalVersions = map[string]string{
"2026.3.12": "0.2.0",
"2026.3.17": "0.3.0",
"2026.3.23": "0.4.0",
"2026.3.28": "0.5.0",
"2026.3.30": "0.6.0",
"2026.4.3": "0.7.0",
"2026.4.8": "0.8.0",
"2026.4.13": "0.9.0",
"2026.4.16": "0.10.0",
"2026.4.23": "0.11.0",
"2026.4.30": "0.12.0",
"2026.5.7": "0.13.0",
"2026.5.16": "0.14.0",
"2026.5.28": "0.15.0",
"2026.5.29": "0.15.1",
"2026.5.29.2": "0.15.2",
"2026.6.5": "0.16.0",
"2026.6.19": "0.17.0",
"2026.7.1": "0.18.0",
"2026.7.7": "0.18.1",
"2026.7.7.2": "0.18.2",
"2026.7.20": "0.19.0",
"2026.7.30": "0.19.1",
"2026.8.3": "0.20.0",
"2026.8.13": "0.20.1",
"2026.8.16": "0.20.2",
"2026.8.16.2": "0.20.3",
"2026.8.18": "0.20.4",
"2026.8.19": "0.20.5",
"2026.8.27": "0.20.6",
"2026.8.31": "0.21.0",
"2026.9.7": "0.21.1",
"2026.9.11": "0.21.2",
"2026.9.14": "0.21.3",
"2026.9.21": "0.21.4",
"2026.9.24": "0.21.5",
}
var hermesReleaseVersion = regexp.MustCompile(`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
func isHermesApp(key string) bool {
return key == constant.AppHermesAgent || key == "local"+constant.AppHermesAgent
}
func hermesComparableVersion(version string) (string, bool) {
version = strings.TrimPrefix(version, "v")
if mapped, ok := hermesHistoricalVersions[version]; ok {
return mapped, true
}
parts := hermesReleaseVersion.FindStringSubmatch(version)
if len(parts) == 0 {
return "", false
}
for i, part := range parts[1:] {
n, err := strconv.Atoi(part)
if err != nil || (i == 0 && n >= 2000) {
// An unknown calendar tag must not become a huge semantic major.
return "", false
}
}
return version, true
}
func isNewerAppVersion(key, target, current string) bool {
if !isHermesApp(key) {
return common.CompareVersion(target, current)
}
targetVersion, targetOK := hermesComparableVersion(target)
currentVersion, currentOK := hermesComparableVersion(current)
return targetOK && currentOK && common.CompareVersion(targetVersion, currentVersion)
}
func isCrossAppVersion(key, current, target string) bool {
if !isHermesApp(key) {
return common.IsCrossVersion(current, target)
}
currentVersion, currentOK := hermesComparableVersion(current)
targetVersion, targetOK := hermesComparableVersion(target)
return currentOK && targetOK && common.IsCrossVersion(currentVersion, targetVersion)
}
func appVersionSortBefore(key, left, right string) bool {
if !isHermesApp(key) {
return common.CompareVersion(left, right)
}
lv, lok := hermesComparableVersion(left)
rv, rok := hermesComparableVersion(right)
if lok != rok {
return lok
}
if !lok {
return common.CompareVersion(left, right)
}
if lv != rv {
return common.CompareVersion(lv, rv)
}
// Prefer the official semantic spelling when both aliases are available.
return strings.TrimPrefix(left, "v") == lv && strings.TrimPrefix(right, "v") != rv
}
func sortAppVersions(key string, versions []string) []string {
if !isHermesApp(key) {
return common.GetSortedVersions(versions)
}
sort.SliceStable(versions, func(i, j int) bool {
return appVersionSortBefore(key, versions[i], versions[j])
})
return versions
}
+31 -16
View File
@@ -28,7 +28,8 @@ 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/docker/docker/pkg/jsonmessage"
"github.com/moby/go-archive"
)
type ImageService struct{}
@@ -325,18 +326,16 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
}
go func() {
client, err := docker.NewDockerClient()
if err != nil {
taskItem.Log("Failed to create Docker client: " + err.Error())
return
}
defer client.Close()
for _, itemPath := range req.Paths {
currentPath := itemPath
itemName := path.Base(currentPath)
taskItem.AddSubTask(i18n.GetWithName("TaskImport", itemName), func(t *task.Task) error {
taskItem.Logf("----------------- %s -----------------", itemName)
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
file, err := os.Open(currentPath)
if err != nil {
return err
@@ -347,14 +346,7 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
return err
}
defer res.Body.Close()
content, err := io.ReadAll(res.Body)
if err != nil {
return err
}
if strings.Contains(string(content), "Error") {
return errors.New(string(content))
}
return nil
return consumeImageLoadResponse(res.Body, taskItem.Log)
}, nil)
}
_ = taskItem.Execute()
@@ -362,6 +354,29 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
return nil
}
// Docker may report load failures in a successful HTTP response's JSON stream.
func consumeImageLoadResponse(reader io.Reader, log func(string)) error {
decoder := json.NewDecoder(reader)
for {
var message jsonmessage.JSONMessage
if err := decoder.Decode(&message); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
if message.Error != nil && message.Error.Message != "" {
return message.Error
}
if message.ErrorMessage != "" {
return errors.New(message.ErrorMessage)
}
if text := strings.TrimSpace(message.Stream); text != "" {
log(text)
}
}
}
func (u *ImageService) ImageSave(req dto.ImageSave) error {
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskExport, task.TaskScopeImage, req.TaskID, 1)
if err != nil {
+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(ctx context.Context, req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (dto.MonitorVLLMCurrentData, error)
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
LoadGPUOptions() dto.MonitorGPUOptions
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...)
}
+309
View File
@@ -0,0 +1,309 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/vllm"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/robfig/cron/v3"
"golang.org/x/sync/errgroup"
)
var vllmMonitorMutex sync.Mutex
var vllmMetricsClient = &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext, IdleConnTimeout: 30 * time.Second},
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
}
func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) {
data := dto.MonitorVLLMData{Points: []model.MonitorVLLM{}}
if !req.EndTime.After(req.StartTime) {
return data, fmt.Errorf("invalid vLLM history request")
}
install, err := appInstallRepo.GetFirstByCtx(ctx, repo.WithByID(req.AppInstallID))
if err != nil {
return data, err
}
if install.App.Key != "vllm" {
return data, fmt.Errorf("not a vLLM installation")
}
loc, err := time.LoadLocation(common.LoadTimeZoneByCmd())
if err != nil {
return data, err
}
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
data.SampleCount, err = vllmMonitorRepo.Count(ctx, req.AppInstallID, req.StartTime, req.EndTime)
if err != nil || data.SampleCount == 0 {
return data, err
}
if data.SampleCount > 1200 {
data.BucketSeconds = (req.EndTime.Unix() - req.StartTime.Unix() + 600) / 600
}
points, err := vllmMonitorRepo.History(ctx, req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation, req.Group)
if err != nil {
return data, err
}
for i := range points {
points[i].MonitorVLLM.CompletedRequests = points[i].CompletedRequests
}
if data.BucketSeconds > 0 {
aggregates := map[int64]*vllm.HistoryAccumulator{}
err = vllmMonitorRepo.WalkDeltas(ctx, req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation, req.Group, func(bucket int64, histograms, requests string) error {
if aggregates[bucket] == nil {
aggregates[bucket] = &vllm.HistoryAccumulator{}
}
return aggregates[bucket].Add(histograms, requests, req.Aggregation, req.Group)
})
if err != nil {
return data, err
}
for i := range points {
if aggregate := aggregates[points[i].Bucket]; aggregate != nil {
aggregate.Apply(&points[i].MonitorVLLM, req.Aggregation)
}
}
next := 0
for bucket := int64(0); bucket <= (req.EndTime.Unix()-req.StartTime.Unix())/data.BucketSeconds; bucket++ {
var point model.MonitorVLLM
if next < len(points) && points[next].Bucket == bucket {
point = points[next].MonitorVLLM
next++
}
point.AppInstallID = req.AppInstallID
point.CreatedAt = time.Unix(req.StartTime.Unix()+bucket*data.BucketSeconds, 0).In(loc)
if bucket == 0 {
point.CreatedAt = req.StartTime
}
data.Points = append(data.Points, point)
}
} else {
for i, point := range points {
if err := ctx.Err(); err != nil {
return data, err
}
var aggregate vllm.HistoryAccumulator
if err := aggregate.Add(point.HistogramDeltas, point.RequestDeltas, "max", req.Group); err != nil {
return data, err
}
aggregate.Apply(&point.MonitorVLLM, "max")
if i > 0 {
previous := points[i-1]
interval := time.Duration(max(previous.SampleIntervalSeconds, point.SampleIntervalSeconds)) * time.Second
if interval == 0 {
if i > 1 {
interval = previous.CreatedAt.Sub(points[i-2].CreatedAt)
}
if i+1 < len(points) {
interval = max(interval, points[i+1].CreatedAt.Sub(point.CreatedAt))
}
}
if interval > 0 && point.CreatedAt.Sub(previous.CreatedAt) > 2*interval {
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: previous.CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
}
}
data.Points = append(data.Points, point.MonitorVLLM)
}
}
return data, nil
}
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (dto.MonitorVLLMCurrentData, error) {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return dto.MonitorVLLMCurrentData{}, err
}
if install.App.Key != "vllm" {
return dto.MonitorVLLMCurrentData{}, fmt.Errorf("not a vLLM installation")
}
point, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
data := dto.MonitorVLLMCurrentData{MonitorVLLM: point}
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
return data, nil
}
if err := json.Unmarshal([]byte(point.RawMetrics), &data.Metrics); err != nil {
return data, err
}
for name := range data.Metrics {
if name == "request_prompt_tokens_sum" || name == "request_prompt_tokens_count" ||
(!strings.HasSuffix(name, "_total") && !strings.HasSuffix(name, "_sum") && !strings.HasSuffix(name, "_count")) {
delete(data.Metrics, name)
}
}
return data, nil
}
func (m *MonitorService) CleanVLLMMonitor(req dto.MonitorVLLMClean) error {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return err
}
if install.App.Key != "vllm" {
return fmt.Errorf("not a vLLM installation")
}
vllmMonitorMutex.Lock()
defer vllmMonitorMutex.Unlock()
return vllmMonitorRepo.CleanTarget(req.AppInstallID)
}
func StartVLLMMonitor(interval string) error {
seconds, err := strconv.Atoi(interval)
if err != nil || seconds < 10 || seconds > 43200 {
return fmt.Errorf("invalid vLLM monitoring interval: %s", interval)
}
service := &MonitorService{}
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(func() { service.saveVLLMData(seconds) }))
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
if err != nil {
return err
}
if global.VLLMMonitorCronID != 0 {
global.Cron.Remove(global.VLLMMonitorCronID)
}
global.VLLMMonitorCronID = id
return nil
}
func (m *MonitorService) saveVLLMData(intervalSeconds int) {
if !vllmMonitorMutex.TryLock() {
return
}
defer vllmMonitorMutex.Unlock()
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
if err != nil {
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
return
}
if status != constant.StatusEnable {
return
}
retention, err := settingRepo.GetValueByKey("VLLMMonitorStoreDays")
if err != nil {
global.LOG.Errorf("Load vLLM monitoring retention failed: %v", err)
return
}
days, err := strconv.Atoi(retention)
if err != nil || days < 1 {
global.LOG.Errorf("Invalid vLLM monitoring retention: %s", retention)
return
}
if err := vllmMonitorRepo.DeleteBefore(time.Now().AddDate(0, 0, -days)); err != nil {
global.LOG.Errorf("Clean vLLM monitoring data failed: %v", err)
}
apps, err := appRepo.GetBy(appRepo.WithKey("vllm"))
if err != nil {
global.LOG.Errorf("Load vLLM apps for monitoring failed: %v", err)
return
}
if len(apps) == 0 {
return
}
ids := make([]uint, 0, len(apps))
for _, app := range apps {
ids = append(ids, app.ID)
}
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppIdsIn(ids))
if err != nil {
global.LOG.Errorf("Load vLLM instances for monitoring failed: %v", err)
return
}
var group errgroup.Group
group.SetLimit(4)
for _, install := range installs {
group.Go(func() error {
previous, err := vllmMonitorRepo.Latest(install.ID)
if err != nil {
return err
}
point, err := collectVLLMMetrics(context.Background(), install, previous)
point.SampleIntervalSeconds = intervalSeconds
if err != nil {
global.LOG.Debugf("Collect vLLM metrics for %s failed: %v", install.Name, err)
}
return vllmMonitorRepo.Create(&point)
})
}
if err := group.Wait(); err != nil {
global.LOG.Errorf("Save vLLM monitoring data failed: %v", err)
}
}
func collectVLLMMetrics(ctx context.Context, install model.AppInstall, previous model.MonitorVLLM) (model.MonitorVLLM, error) {
point := model.MonitorVLLM{AppInstallID: install.ID, CreatedAt: time.Now(), Status: "unavailable"}
if install.HttpPort <= 0 || install.HttpPort > 65535 {
return point, fmt.Errorf("invalid vLLM port")
}
var env map[string]interface{}
if err := json.Unmarshal([]byte(install.Env), &env); err != nil {
return point, err
}
host, _ := env[constant.HostIP].(string)
host = strings.Trim(host, "[]")
switch host {
case "", "0.0.0.0":
host = "127.0.0.1"
case "::":
host = "::1"
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+net.JoinHostPort(host, strconv.Itoa(install.HttpPort))+"/metrics", nil)
if err != nil {
return point, err
}
response, err := vllmMetricsClient.Do(req)
if err != nil {
return point, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return point, fmt.Errorf("vLLM metrics returned HTTP %d", response.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024+1))
if err != nil {
return point, err
}
if len(body) > 8*1024*1024 {
return point, fmt.Errorf("vLLM metrics response too large")
}
metrics, err := vllm.Parse(bytes.NewReader(body))
if err != nil {
return point, err
}
point.CreatedAt = time.Now()
raw, err := json.Marshal(metrics)
if err != nil {
return point, err
}
var before vllm.Metrics
elapsed := point.CreatedAt.Sub(previous.CreatedAt).Seconds()
if previous.Status == "ok" && elapsed > 0 {
if err := json.Unmarshal([]byte(previous.RawMetrics), &before); err != nil {
return point, err
}
}
calculated, err := vllm.Calculate(metrics, before, elapsed)
if err != nil {
return point, err
}
calculated.AppInstallID = install.ID
calculated.CreatedAt = point.CreatedAt
calculated.RawMetrics = string(raw)
calculated.Status = "ok"
return calculated, nil
}
+49
View File
@@ -0,0 +1,49 @@
package service
import (
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
)
func (u *DashboardService) ListQuickDirectories() ([]model.QuickDirectory, error) {
return launcherRepo.ListQuickDirectories()
}
func quickDirectoryValues(req dto.QuickDirectoryCreate) (model.QuickDirectory, error) {
item := model.QuickDirectory{Path: req.Path, Alias: strings.TrimSpace(req.Alias)}
if strings.TrimSpace(item.Path) == "" {
return item, buserr.WithDetail("ErrInvalidParams", "path is required", nil)
}
return item, nil
}
func (u *DashboardService) CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error) {
item, err := quickDirectoryValues(req)
if err != nil {
return nil, err
}
if err := launcherRepo.CreateQuickDirectory(&item); err != nil {
return nil, err
}
return &item, nil
}
func (u *DashboardService) UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error) {
item, err := quickDirectoryValues(req.QuickDirectoryCreate)
if err != nil {
return nil, err
}
item.ID = req.ID
item.Legacy = req.Legacy
if err := launcherRepo.UpdateQuickDirectory(&item); err != nil {
return nil, err
}
return &item, nil
}
func (u *DashboardService) DeleteQuickDirectory(req dto.QuickDirectoryDelete) error {
return launcherRepo.DeleteQuickDirectory(req.ID, req.Legacy)
}
+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
}
+3
View File
@@ -494,6 +494,9 @@ func restartCompose(composePath string, itemHelper *snapRecoverHelper) error {
func recoverDBData(src string, itemHelper *snapRecoverHelper) error {
itemHelper.Task.Log("---------------------- 11 / 11 ----------------------")
itemHelper.Task.LogStart(i18n.GetMsgByKey("RecoverDBData"))
if err := os.Chmod(src, 0700); err != nil {
return err
}
dbDir := path.Join(global.Dir.DataDir, "db")
if err := os.RemoveAll(dbDir); err != nil {
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("RecoverDBData"), err)
+3
View File
@@ -106,6 +106,9 @@ func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error {
taskItem.AddSubTask(
i18n.GetWithName("SnapCopy", global.Dir.BaseDir),
func(t *task.Task) error {
if err := os.Chmod(path.Join(rootDir, "1panel/db"), 0700); err != nil {
return err
}
return FileOp.CopyDir(path.Join(rootDir, "1panel"), global.Dir.BaseDir)
},
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 {
+4 -1
View File
@@ -57,7 +57,10 @@ func (u *TaskLogService) ReadByLine(req request.TaskLogReadReq) (*response.FileL
if req.TaskID != "" {
opts = append(opts, taskRepo.WithByID(req.TaskID))
} else {
opts = append(opts, repo.WithOrderRuleBy("created_at", "desc"), repo.WithByType(req.TaskType), taskRepo.WithOperate(req.TaskOperate), taskRepo.WithResourceID(req.ResourceID))
opts = append(opts, repo.WithOrderRuleBy("created_at", "desc"), repo.WithByType(req.TaskType), taskRepo.WithResourceID(req.ResourceID))
if req.TaskOperate != "" {
opts = append(opts, taskRepo.WithOperate(req.TaskOperate))
}
}
taskModel, err := taskRepo.GetFirst(opts...)
if err != nil {
+26 -8
View File
@@ -526,10 +526,8 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
website.SiteDir = create.SiteDir
}
createdFiles := &websiteCreateFiles{}
configNginx := func(t *task.Task) error {
if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig, initialSSL); err != nil {
return err
}
if create.Type == constant.Static && create.TemplateOutputID > 0 {
templateOutput, err := websiteTemplateOutputRepo.GetFirst(repo.WithByID(create.TemplateOutputID))
if err != nil {
@@ -541,12 +539,15 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
if _, err := os.Stat(templateOutput.OutputPath); err != nil {
return buserr.New("ErrFileNotFound")
}
if err := copyDir(templateOutput.OutputPath, GetSitePath(*website, SiteIndexDir)); err != nil {
if err := createdFiles.copyTemplate(templateOutput.OutputPath, GetSitePath(*website, SiteIndexDir)); err != nil {
return err
}
}
if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig, initialSSL, createdFiles); err != nil {
return err
}
if website.Type != constant.Stream {
if err = createWafConfig(website, domains); err != nil {
if err = createWafConfig(website, domains, createdFiles); err != nil {
return err
}
if create.Type == constant.Runtime {
@@ -555,11 +556,21 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
return err
}
if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceAppstore {
createOpenBasedirConfig(website)
if err := createdFiles.write(path.Join(GetSitePath(*website, SiteIndexDir), ".user.ini"), []byte(fmt.Sprintf("open_basedir=/www/sites/%s/index:/tmp/", website.Alias)), 0644, true); err != nil {
return err
}
}
}
}
// Apply the site only after all Nginx, WAF, and PHP configuration is ready.
if err := opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil {
return err
}
if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
return err
}
tx, ctx := helper.GetTxAndContext()
defer tx.Rollback()
if err = websiteRepo.Create(ctx, website); err != nil {
@@ -575,12 +586,19 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
deleteWebsite := func(t *task.Task) {
_ = deleteWebsiteFolder(website)
if rollbackErr := createdFiles.rollback(); rollbackErr != nil {
t.Log(fmt.Sprintf("website file rollback: %v", rollbackErr))
}
if reloadErr := opNginx(nginxInstall.ContainerName, constant.NginxReload); reloadErr != nil {
t.Log(fmt.Sprintf("reload OpenResty after rollback: %v", reloadErr))
}
}
createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite)
+12 -3
View File
@@ -212,10 +212,17 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
websiteSSL.Shell = req.Shell
}
if req.PushDir {
if !files.NewFileOp().Stat(req.Dir) {
dirs := certificateDirectories(req.Dir)
if len(dirs) == 0 {
return nil, buserr.New("ErrLinkPathNotFound")
}
websiteSSL.Dir = req.Dir
for _, dir := range dirs {
info, err := os.Stat(dir)
if err != nil || !info.IsDir() {
return nil, buserr.New("ErrLinkPathNotFound")
}
}
websiteSSL.Dir = strings.Join(dirs, "\n")
}
if req.Domains != "" {
domainArray := strings.Split(req.Domains, "\n")
@@ -348,7 +355,9 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
if websiteSSL.ExecShell {
workDir := global.Dir.DataDir
if websiteSSL.PushDir {
workDir = websiteSSL.Dir
if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
workDir = dirs[0]
}
}
logger.Println(i18n.GetMsgByKey("ExecShellStart"))
if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
+49 -74
View File
@@ -2,7 +2,6 @@ package service
import (
"context"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -46,39 +45,27 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
fileOp := files.NewFileOp()
if fileOp.Stat(wafDataPath) {
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
content, err := fileOp.GetContent(websitesConfigPath)
if err != nil {
return nil, err
}
var websitesArray []request.WafWebsite
if content != nil {
if err := json.Unmarshal(content, &websitesArray); err != nil {
return nil, err
if err := updateWAFWebsites(websitesConfigPath, func(websitesArray []request.WafWebsite) ([]request.WafWebsite, error) {
for index, wafWebsite := range websitesArray {
if wafWebsite.Key == website.Alias {
wafSite := request.WafWebsite{
Key: website.Alias,
Domains: wafWebsite.Domains,
Host: wafWebsite.Host,
}
for _, domain := range domainModels {
wafSite.Domains = append(wafSite.Domains, domain.Domain)
wafSite.Host = append(wafSite.Host, domain.Domain+":"+strconv.Itoa(domain.Port))
}
if len(wafSite.Host) == 0 {
wafSite.Host = []string{}
}
websitesArray[index] = wafSite
break
}
}
}
for index, wafWebsite := range websitesArray {
if wafWebsite.Key == website.Alias {
wafSite := request.WafWebsite{
Key: website.Alias,
Domains: wafWebsite.Domains,
Host: wafWebsite.Host,
}
for _, domain := range domainModels {
wafSite.Domains = append(wafSite.Domains, domain.Domain)
wafSite.Host = append(wafSite.Host, domain.Domain+":"+strconv.Itoa(domain.Port))
}
if len(wafSite.Host) == 0 {
wafSite.Host = []string{}
}
websitesArray[index] = wafSite
break
}
}
websitesContent, err := json.Marshal(websitesArray)
if err != nil {
return nil, err
}
if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
return websitesArray, nil
}); err != nil {
return nil, err
}
}
@@ -135,51 +122,39 @@ func (w WebsiteService) DeleteWebsiteDomain(domainId uint) error {
fileOp := files.NewFileOp()
if fileOp.Stat(wafDataPath) {
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
content, err := fileOp.GetContent(websitesConfigPath)
if err != nil {
return err
}
var websitesArray []request.WafWebsite
var newWebsitesArray []request.WafWebsite
if content != nil {
if err := json.Unmarshal(content, &websitesArray); err != nil {
return err
}
}
for _, wafWebsite := range websitesArray {
if wafWebsite.Key == website.Alias {
wafSite := wafWebsite
oldDomains := wafSite.Domains
var newDomains []string
for _, domain := range oldDomains {
if domain == webSiteDomain.Domain {
continue
if err := updateWAFWebsites(websitesConfigPath, func(websitesArray []request.WafWebsite) ([]request.WafWebsite, error) {
var newWebsitesArray []request.WafWebsite
for _, wafWebsite := range websitesArray {
if wafWebsite.Key == website.Alias {
wafSite := wafWebsite
oldDomains := wafSite.Domains
var newDomains []string
for _, domain := range oldDomains {
if domain == webSiteDomain.Domain {
continue
}
newDomains = append(newDomains, domain)
}
newDomains = append(newDomains, domain)
}
wafSite.Domains = newDomains
oldHostArray := wafSite.Host
var newHostArray []string
for _, host := range oldHostArray {
if host == webSiteDomain.Domain+":"+strconv.Itoa(webSiteDomain.Port) {
continue
wafSite.Domains = newDomains
oldHostArray := wafSite.Host
var newHostArray []string
for _, host := range oldHostArray {
if host == webSiteDomain.Domain+":"+strconv.Itoa(webSiteDomain.Port) {
continue
}
newHostArray = append(newHostArray, host)
}
newHostArray = append(newHostArray, host)
wafSite.Host = newHostArray
if len(wafSite.Host) == 0 {
wafSite.Host = []string{}
}
newWebsitesArray = append(newWebsitesArray, wafSite)
} else {
newWebsitesArray = append(newWebsitesArray, wafWebsite)
}
wafSite.Host = newHostArray
if len(wafSite.Host) == 0 {
wafSite.Host = []string{}
}
newWebsitesArray = append(newWebsitesArray, wafSite)
} else {
newWebsitesArray = append(newWebsitesArray, wafWebsite)
}
}
websitesContent, err := json.Marshal(newWebsitesArray)
if err != nil {
return err
}
if err = fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
return newWebsitesArray, nil
}); err != nil {
return err
}
}
+21 -8
View File
@@ -209,11 +209,17 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
websiteSSL.Shell = create.Shell
}
if create.PushDir {
fileOP := files.NewFileOp()
if !fileOP.Stat(create.Dir) {
dirs := certificateDirectories(create.Dir)
if len(dirs) == 0 {
return res, buserr.New("ErrLinkPathNotFound")
}
websiteSSL.Dir = create.Dir
for _, dir := range dirs {
info, err := os.Stat(dir)
if err != nil || !info.IsDir() {
return res, buserr.New("ErrLinkPathNotFound")
}
}
websiteSSL.Dir = strings.Join(dirs, "\n")
}
setSSLPushConfig(&websiteSSL, create.PushNode, create.Nodes)
@@ -573,7 +579,9 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
if websiteSSL.ExecShell {
workDir := global.Dir.DataDir
if websiteSSL.PushDir {
workDir = websiteSSL.Dir
if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
workDir = dirs[0]
}
}
printSSLLog(logger, "ExecShellStart", nil)
if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
@@ -793,11 +801,16 @@ func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error {
}
if update.PushDir {
fileOP := files.NewFileOp()
if !fileOP.Stat(update.Dir) {
_ = fileOP.CreateDir(update.Dir, constant.DirPerm)
dirs := certificateDirectories(update.Dir)
if len(dirs) == 0 {
return buserr.New("ErrLinkPathNotFound")
}
updateParams["dir"] = update.Dir
for _, dir := range dirs {
if err := os.MkdirAll(dir, constant.DirPerm); err != nil {
return err
}
}
updateParams["dir"] = strings.Join(dirs, "\n")
}
var domains []string
if update.OtherDomains != "" {
-35
View File
@@ -403,38 +403,3 @@ func renderMainHTMLFromZip(zipPath string, values map[string]string) (string, er
}
return renderContent(string(content), values), nil
}
func copyDir(src, dst string) error {
return filepath.Walk(src, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(src, p)
if err != nil {
return err
}
targetPath := filepath.Join(dst, relPath)
if info.IsDir() {
return os.MkdirAll(targetPath, constant.DirPerm)
}
return copyFile(p, targetPath)
})
}
func copyFile(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
if err := os.MkdirAll(filepath.Dir(dst), constant.DirPerm); err != nil {
return err
}
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
return err
}
+353 -152
View File
@@ -1,11 +1,13 @@
package service
import (
"bytes"
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"log"
"net"
"os"
@@ -14,6 +16,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
"syscall"
"time"
@@ -124,7 +127,257 @@ func applyLocationProxyPass(location *components.Location, proxyPass string, sni
location.UpdateDirective("proxy_ssl_name", []string{sslName})
}
func createIndexFile(website *model.Website, runtime *model.Runtime) error {
// websiteCreateFiles owns only paths actually created by this attempt.
// Never recursively remove a reused directory (or a newly populated one).
type websiteCreateFiles struct {
mu sync.Mutex
entries []websiteCreatedPath
rolledBack bool
restores []func() error
}
type websiteCreatedPath struct {
path string
info os.FileInfo
}
func (f *websiteCreateFiles) mkdir(name string) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
return f.mkdirLocked(filepath.Clean(name))
}
func (f *websiteCreateFiles) mkdirLocked(name string) error {
info, err := os.Lstat(name)
if err == nil {
if !info.IsDir() {
return fmt.Errorf("website path is not a directory: %s", name)
}
return nil
}
if !os.IsNotExist(err) {
return err
}
if err = f.mkdirLocked(filepath.Dir(name)); err != nil {
return err
}
if err = os.Mkdir(name, constant.DirPerm); err != nil {
if os.IsExist(err) {
return f.mkdirLocked(name)
}
return err
}
info, err = os.Lstat(name)
if err != nil {
return err
}
f.entries = append(f.entries, websiteCreatedPath{name, info})
return nil
}
// preserveExisting is for user content and logs; generated configuration must
// fail on collision. Exclusive creation also protects against concurrent writes.
func (f *websiteCreateFiles) write(name string, data []byte, mode os.FileMode, preserveExisting bool) error {
return f.writeReader(name, bytes.NewReader(data), mode, preserveExisting)
}
func (f *websiteCreateFiles) writeReader(name string, data io.Reader, mode os.FileMode, preserveExisting bool) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
if err := f.mkdirLocked(filepath.Dir(name)); err != nil {
return err
}
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
if os.IsExist(err) && preserveExisting {
info, statErr := os.Lstat(name)
if statErr != nil {
return statErr
}
if !info.Mode().IsRegular() {
return fmt.Errorf("website path is not a regular file: %s", name)
}
return nil
}
if err != nil {
return fmt.Errorf("create website file %s: %w", name, err)
}
info, err := file.Stat()
if err != nil {
_ = file.Close()
return err
}
f.entries = append(f.entries, websiteCreatedPath{name, info})
_, err = io.Copy(file, data)
closeErr := file.Close()
if err != nil {
return err
}
return closeErr
}
func (f *websiteCreateFiles) rollback() error {
f.mu.Lock()
defer f.mu.Unlock()
f.rolledBack = true
var firstErr error
for i := len(f.restores) - 1; i >= 0; i-- {
if err := f.restores[i](); err != nil && firstErr == nil {
firstErr = err
}
}
f.restores = nil
for i := len(f.entries) - 1; i >= 0; i-- {
entry := f.entries[i]
info, err := os.Lstat(entry.path)
if os.IsNotExist(err) {
continue
}
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
if !os.SameFile(entry.info, info) {
continue
}
// os.Remove refuses non-empty directories, preserving untracked content.
if err = os.Remove(entry.path); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
func (f *websiteCreateFiles) copyTemplate(src, dst string) error {
// Reject symlinks and create files exclusively. The caller rolls back any
// paths created before a collision or copy error.
return filepath.Walk(src, func(name string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(src, name)
if err != nil {
return err
}
target := filepath.Join(dst, rel)
if info.IsDir() {
return f.mkdir(target)
}
if !info.Mode().IsRegular() {
return fmt.Errorf("unsupported template file: %s", name)
}
file, err := os.Open(name)
if err != nil {
return err
}
defer file.Close()
return f.writeReader(target, file, constant.FilePerm, false)
})
}
// Only change ownership on files created by this attempt, never recursively on
// an existing document root.
func (f *websiteCreateFiles) chownNew(root string) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
for _, entry := range f.entries {
rel, err := filepath.Rel(root, entry.path)
if err != nil {
return err
}
if rel == ".." || filepath.IsAbs(rel) || (len(rel) > 3 && rel[:3] == "../") {
continue
}
info, err := os.Lstat(entry.path)
if err != nil {
return err
}
if !os.SameFile(entry.info, info) {
continue
}
if err := os.Chown(entry.path, constant.WebsiteUID, constant.WebsiteGID); err != nil {
return err
}
}
return nil
}
// Serialize sites.json read-modify-write operations across website creations,
// deletions, domain updates, and creation rollbacks.
var wafWebsitesMu sync.Mutex
func updateWAFWebsites(name string, update func([]request.WafWebsite) ([]request.WafWebsite, error)) error {
wafWebsitesMu.Lock()
defer wafWebsitesMu.Unlock()
info, err := os.Lstat(name)
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("website config is not a regular file: %s", name)
}
content, err := os.ReadFile(name)
if err != nil {
return err
}
var websites []request.WafWebsite
if len(content) > 0 {
if err := json.Unmarshal(content, &websites); err != nil {
return err
}
}
websites, err = update(websites)
if err != nil {
return err
}
content, err = json.Marshal(websites)
if err != nil {
return err
}
return os.WriteFile(name, content, info.Mode().Perm())
}
func (f *websiteCreateFiles) addWAFWebsite(name string, website request.WafWebsite) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
if err := updateWAFWebsites(name, func(websites []request.WafWebsite) ([]request.WafWebsite, error) {
for _, existing := range websites {
if existing.Key == website.Key {
return nil, fmt.Errorf("WAF website already exists: %s", website.Key)
}
}
return append(websites, website), nil
}); err != nil {
return err
}
f.restores = append(f.restores, func() error {
return updateWAFWebsites(name, func(websites []request.WafWebsite) ([]request.WafWebsite, error) {
// Keep the latest records for other sites, including concurrent domain changes.
remaining := make([]request.WafWebsite, 0, len(websites))
for _, existing := range websites {
if existing.Key != website.Key {
remaining = append(remaining, existing)
}
}
return remaining, nil
})
})
return nil
}
func createIndexFile(website *model.Website, runtime *model.Runtime, created *websiteCreateFiles) error {
var (
indexPath string
indexContent string
@@ -145,49 +398,23 @@ func createIndexFile(website *model.Website, runtime *model.Runtime) error {
}
}
fileOp := files.NewFileOp()
if !fileOp.Stat(indexFolder) {
if err := fileOp.CreateDir(indexFolder, constant.DirPerm); err != nil {
return err
}
if err := created.write(indexPath, []byte(indexContent), constant.FilePerm, true); err != nil {
return err
}
if !fileOp.Stat(indexPath) {
if err := fileOp.CreateFile(indexPath); err != nil {
return err
}
html404, _ := websiteService.GetDefaultHtml("404")
if err := created.write(path.Join(indexFolder, "404.html"), []byte(html404.Content), constant.FilePerm, true); err != nil {
return err
}
if website.Type == constant.Runtime && runtime.Resource == constant.ResourceAppstore {
if err := chownRootDir(indexFolder); err != nil {
return err
}
}
if err := fileOp.WriteFile(indexPath, strings.NewReader(indexContent), constant.DirPerm); err != nil {
return err
}
html404, _ := websiteService.GetDefaultHtml("404")
path404 := path.Join(indexFolder, "404.html")
if err := fileOp.WriteFile(path404, strings.NewReader(html404.Content), constant.DirPerm); err != nil {
return err
return created.chownNew(indexFolder)
}
return nil
}
func createProxyFile(website *model.Website) error {
func createProxyFile(website *model.Website, created *websiteCreateFiles) error {
proxyFolder := GetSitePath(*website, SiteProxyDir)
filePath := path.Join(proxyFolder, "root.conf")
fileOp := files.NewFileOp()
if !fileOp.Stat(proxyFolder) {
if err := fileOp.CreateDir(proxyFolder, constant.DirPerm); err != nil {
return err
}
}
if !fileOp.Stat(filePath) {
if err := fileOp.CreateFile(filePath); err != nil {
return err
}
}
config, err := parser.NewStringParser(string(nginx_conf.Proxy)).Parse()
if err != nil {
return err
@@ -201,57 +428,40 @@ func createProxyFile(website *model.Website) error {
location.ChangePath("^~", "/")
applyLocationProxyPass(location, website.Proxy, nil, "")
location.UpdateDirective("proxy_set_header", []string{"Host", "$host"})
if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
if err := created.write(filePath, []byte(nginx.DumpConfig(config, nginx.IndentedStyle)), constant.FilePerm, false); err != nil {
return buserr.WithErr("ErrUpdateBuWebsite", err)
}
return nil
}
func createWebsiteFolder(website *model.Website, runtime *model.Runtime) error {
func createWebsiteFolder(website *model.Website, runtime *model.Runtime, created *websiteCreateFiles) error {
siteFolder := GetSiteDir(website.Alias)
fileOp := files.NewFileOp()
if !fileOp.Stat(siteFolder) {
if err := fileOp.CreateDir(siteFolder, constant.DirPerm); err != nil {
dirs := []string{siteFolder, path.Join(siteFolder, "log")}
if website.Type != constant.Stream {
dirs = append(dirs, path.Join(siteFolder, "index"), path.Join(siteFolder, "ssl"))
}
for _, dir := range dirs {
if err := created.mkdir(dir); err != nil {
return err
}
if err := fileOp.CreateDir(path.Join(siteFolder, "log"), constant.DirPerm); err != nil {
}
for _, name := range []string{"access.log", "error.log"} {
if err := created.write(path.Join(siteFolder, "log", name), nil, constant.FilePerm, true); err != nil {
return err
}
if err := fileOp.CreateFile(path.Join(siteFolder, "log", "access.log")); err != nil {
}
if website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceLocal {
if err := created.write(path.Join(siteFolder, "php-pool", "php-fpm.sock"), nil, constant.FilePerm, true); err != nil {
return err
}
if err := fileOp.CreateFile(path.Join(siteFolder, "log", "error.log")); err != nil {
}
if website.Type == constant.Static || (website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP) {
if err := createIndexFile(website, runtime, created); err != nil {
return err
}
if website.Type != constant.Stream {
if err := fileOp.CreateDir(path.Join(siteFolder, "index"), constant.DirPerm); err != nil {
return err
}
if err := fileOp.CreateDir(path.Join(siteFolder, "ssl"), constant.DirPerm); err != nil {
return err
}
}
if website.Type == constant.Runtime {
if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceLocal {
phpPoolDir := path.Join(siteFolder, "php-pool")
if err := fileOp.CreateDir(phpPoolDir, constant.DirPerm); err != nil {
return err
}
if err := fileOp.CreateFile(path.Join(phpPoolDir, "php-fpm.sock")); err != nil {
return err
}
}
}
if website.Type == constant.Static || (website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP) {
if err := createIndexFile(website, runtime); err != nil {
return err
}
}
if website.Type == constant.Proxy {
if err := createProxyFile(website); err != nil {
return err
}
}
}
if website.Type == constant.Proxy {
return createProxyFile(website, created)
}
return nil
}
@@ -261,12 +471,12 @@ type websiteInitialSSL struct {
request request.WebsiteHTTPSOp
}
func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig, initialSSL *websiteInitialSSL) error {
func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig, initialSSL *websiteInitialSSL, created *websiteCreateFiles) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
if err = createWebsiteFolder(website, runtime); err != nil {
if err = createWebsiteFolder(website, runtime, created); err != nil {
return err
}
var (
@@ -334,7 +544,10 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
if initialSSL != nil {
plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort)
applyWebsiteSSLConfig(server, *website, plan, initialSSL.request)
if err = createPemFile(*website, initialSSL.certificate); err != nil {
if err = created.write(path.Join(GetSitePath(*website, SiteSSLDir), "fullchain.pem"), []byte(initialSSL.certificate.Pem), constant.FilePerm, false); err != nil {
return err
}
if err = created.write(path.Join(GetSitePath(*website, SiteSSLDir), "privkey.pem"), []byte(initialSSL.certificate.PrivateKey), 0600, false); err != nil {
return err
}
}
@@ -404,18 +617,14 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
}
}
config.FilePath = configPath
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return err
}
if err = opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil {
return err
}
if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
if err = created.write(configPath, []byte(nginx.DumpConfig(config, nginx.IndentedStyle)), constant.FilePerm, false); err != nil {
return err
}
return nil
}
var defaultWAFRuleNames = []string{"acl", "args", "cookie", "defaultUaBlack", "defaultUrlBlack", "fileExt", "header", "methodWhite", "cdn"}
func moveDefaultWafConfig(websiteDir string, defaultConfigContent []byte, defaultRuleDir string, fileOp files.FileOp) error {
if !fileOp.Stat(websiteDir) {
if err := fileOp.CreateDir(websiteDir, constant.DirPerm); err != nil {
@@ -431,8 +640,7 @@ func moveDefaultWafConfig(websiteDir string, defaultConfigContent []byte, defaul
return err
}
}
defaultRulesName := []string{"acl", "args", "cookie", "defaultUaBlack", "defaultUrlBlack", "fileExt", "header", "methodWhite", "cdn"}
for _, ruleName := range defaultRulesName {
for _, ruleName := range defaultWAFRuleNames {
srcPath := path.Join(defaultRuleDir, ruleName+".json")
if fileOp.Stat(srcPath) {
_ = fileOp.Copy(srcPath, websiteRuleDir)
@@ -470,7 +678,10 @@ func createAllWebsitesWAFConfig(websites []model.Website) error {
if err != nil {
return err
}
if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
wafWebsitesMu.Lock()
err = fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm)
wafWebsitesMu.Unlock()
if err != nil {
return err
}
var (
@@ -499,7 +710,7 @@ func createOpenBasedirConfig(website *model.Website) {
_ = fileOp.SaveFile(userIniPath, fmt.Sprintf("open_basedir=/www/sites/%s/index:/tmp/", website.Alias), 0644)
}
func createWafConfig(website *model.Website, domains []model.WebsiteDomain) error {
func createWafConfig(website *model.Website, domains []model.WebsiteDomain, created *websiteCreateFiles) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
@@ -510,16 +721,6 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
return nil
}
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
content, err := fileOp.GetContent(websitesConfigPath)
if err != nil {
return err
}
var websitesArray []request.WafWebsite
if len(content) != 0 {
if err := json.Unmarshal(content, &websitesArray); err != nil {
return err
}
}
wafWebsite := request.WafWebsite{
Key: website.Alias,
Domains: make([]string, 0),
@@ -530,14 +731,6 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
wafWebsite.Domains = append(wafWebsite.Domains, domain.Domain)
wafWebsite.Host = append(wafWebsite.Host, domain.Domain+":"+strconv.Itoa(domain.Port))
}
websitesArray = append(websitesArray, wafWebsite)
websitesContent, err := json.Marshal(websitesArray)
if err != nil {
return err
}
if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
return err
}
var (
sitesDir = path.Join(wafDataPath, "sites")
@@ -551,20 +744,25 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
return err
}
defer func() {
if err := created.write(path.Join(websiteDir, "config.json"), defaultConfigContent, constant.FilePerm, false); err != nil {
return err
}
if err := created.mkdir(path.Join(websiteDir, "rules")); err != nil {
return err
}
for _, name := range defaultWAFRuleNames {
data, err := os.ReadFile(path.Join(defaultRuleDir, name+".json"))
if os.IsNotExist(err) {
continue
}
if err != nil {
_ = fileOp.DeleteDir(websiteDir)
return err
}
if err := created.write(path.Join(websiteDir, "rules", name+".json"), data, constant.FilePerm, false); err != nil {
return err
}
}()
if err := moveDefaultWafConfig(websiteDir, defaultConfigContent, defaultRuleDir, fileOp); err != nil {
return err
}
if err = opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil {
return err
}
if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
if err := created.addWAFWebsite(websitesConfigPath, wafWebsite); err != nil {
return err
}
@@ -627,27 +825,15 @@ func delWafConfig(website model.Website, force bool) error {
return nil
}
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
content, err := fileOp.GetContent(websitesConfigPath)
if err != nil {
return err
}
var websitesArray []request.WafWebsite
var newWebsiteArray []request.WafWebsite
if len(content) > 0 {
if err = json.Unmarshal(content, &websitesArray); err != nil {
return err
if err := updateWAFWebsites(websitesConfigPath, func(websites []request.WafWebsite) ([]request.WafWebsite, error) {
remaining := make([]request.WafWebsite, 0, len(websites))
for _, existing := range websites {
if existing.Key != website.Alias {
remaining = append(remaining, existing)
}
}
}
for _, wafWebsite := range websitesArray {
if wafWebsite.Key != website.Alias {
newWebsiteArray = append(newWebsiteArray, wafWebsite)
}
}
websitesContent, err := json.Marshal(newWebsiteArray)
if err != nil {
return err
}
if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
return remaining, nil
}); err != nil {
return err
}
@@ -1343,26 +1529,41 @@ func checkWebsitePort(defaultHTTPsPort, port int, websiteType string) error {
return nil
}
// certificateDirectories accepts both legacy single paths and newline-separated paths.
func certificateDirectories(value string) []string {
var dirs []string
seen := make(map[string]bool)
for _, dir := range strings.Split(value, "\n") {
dir = strings.TrimSpace(dir)
if dir == "" {
continue
}
dir = filepath.Clean(dir)
if !seen[dir] {
seen[dir] = true
dirs = append(dirs, dir)
}
}
return dirs
}
func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
if websiteSSL.PushDir {
fileOp := files.NewFileOp()
var (
pushErr error
MsgMap = map[string]interface{}{"path": websiteSSL.Dir, "status": i18n.GetMsgByKey("Success")}
)
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "privkey.pem"), websiteSSL.PrivateKey, constant.FilePerm); pushErr != nil {
MsgMap["status"] = i18n.GetMsgByKey("Failed")
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
logger.Println("Push dir failed:" + pushErr.Error())
}
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "fullchain.pem"), websiteSSL.Pem, constant.FilePerm); pushErr != nil {
MsgMap["status"] = i18n.GetMsgByKey("Failed")
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
logger.Println("Push dir failed:" + pushErr.Error())
}
if pushErr == nil {
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
if !websiteSSL.PushDir {
return
}
fileOp := files.NewFileOp()
for _, dir := range certificateDirectories(websiteSSL.Dir) {
status := i18n.GetMsgByKey("Success")
for _, file := range []struct{ name, content string }{
{"privkey.pem", websiteSSL.PrivateKey},
{"fullchain.pem", websiteSSL.Pem},
} {
if err := fileOp.SaveFile(path.Join(dir, file.name), file.content, constant.FilePerm); err != nil {
status = i18n.GetMsgByKey("Failed")
logger.Printf("Push dir %s failed: %s", dir, err)
}
}
logger.Println(i18n.GetMsgWithMap("PushDirLog", map[string]interface{}{"path": dir, "status": status}))
}
}
+10 -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)
}
@@ -257,7 +258,15 @@ func (s *SubTask) Execute() error {
case <-ctx.Done():
s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
err = errors.New("timeout!")
if s.RootTask.Task.Type == TaskScopeVm {
<-done
}
case err = <-done:
if s.RootTask.Task.Type == TaskScopeVm && ctx.Err() != nil {
s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
err = errors.New("timeout!")
break
}
if err != nil {
s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", subTaskName, err))
if err.Error() == i18n.GetMsgByKey("ErrShutDown") {
+330 -323
View File
@@ -108,33 +108,29 @@
"formatEN": "create alert [title]"
},
"/alert/config/del": {
"bodyKeys": [
"id"
],
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除告警配置 [id]",
"formatEN": "delete alert config [id]"
"formatZH": "删除告警配置 [name]",
"formatEN": "delete alert config [name]",
"beforeFunctions": []
},
"/alert/config/status": {
"bodyKeys": [
"id",
"status"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置状态 [id][status]",
"formatEN": "update alert config status [id][status]"
"formatZH": "更新告警配置状态 [name][status]",
"formatEN": "update alert config status [name][status]",
"beforeFunctions": []
},
"/alert/config/update": {
"bodyKeys": [
"id",
"displayName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置 [id][displayName]",
"formatEN": "update alert config [id][displayName]"
"formatZH": "更新告警配置 [displayName]",
"formatEN": "update alert config [displayName]",
"beforeFunctions": []
},
"/alert/del": {
"bodyKeys": [
@@ -2459,271 +2455,6 @@
"formatZH": "同步 SSL 证书 [primaryDomain]",
"formatEN": "sync SSL certificate [primaryDomain]"
},
"/core/xpack/vms": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/core/xpack/vms/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "virtual_machines",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/core/xpack/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment"
},
"/core/xpack/vms/iso": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/core/xpack/vms/iso/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_isos",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/core/xpack/vms/iso/update": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/core/xpack/vms/networks": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/core/xpack/vms/networks/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/core/xpack/vms/networks/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/core/xpack/vms/operate": {
"bodyKeys": [
"name",
"operate"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/core/xpack/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/core/xpack/vms/rename": {
"bodyKeys": [
"name",
"newName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/core/xpack/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/core/xpack/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/core/xpack/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/core/xpack/vms/storages": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/core/xpack/vms/storages/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/core/xpack/vms/storages/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/core/xpack/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/core/xpack/vms/templates": {
"bodyKeys": [
"name",
"templateName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/core/xpack/vms/templates/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_templates",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/core/xpack/vms/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机 [name]",
"formatEN": "update VM [name]"
},
"/cronjobs": {
"bodyKeys": [
"type",
@@ -3662,20 +3393,22 @@
"formatEN": "batch update Docker port guard policies"
},
"/hosts/firewall/docker/policies/delete/batch": {
"bodyKeys": [
"uuids"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除 Docker 端口防护策略 [uuids]",
"formatEN": "delete Docker port guard policies [uuids]"
},
"/hosts/firewall/docker/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步 Docker 端口防护规则",
"formatEN": "sync Docker port guard rules"
"formatZH": "删除 Docker 端口防护策略",
"formatEN": "delete Docker port guard policies"
},
"/hosts/firewall/family/operate": {
"bodyKeys": [
"subsystem",
"family",
"operation"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "[operation] [subsystem] [family] 防火墙链",
"formatEN": "[operation] [subsystem] [family] firewall chains"
},
"/hosts/firewall/filter/operate": {
"bodyKeys": [
@@ -3716,13 +3449,6 @@
"formatZH": "添加防火墙规则",
"formatEN": "create firewall rules"
},
"/hosts/firewall/rules/adopt": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "纳管防火墙规则",
"formatEN": "adopt firewall rule"
},
"/hosts/firewall/rules/delete": {
"bodyKeys": [],
"paramKeys": [],
@@ -3731,13 +3457,11 @@
"formatEN": "delete firewall rules"
},
"/hosts/firewall/rules/reorder": {
"bodyKeys": [
"uuid"
],
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "调整防火墙规则顺序 [uuid]",
"formatEN": "reorder firewall rule [uuid]"
"formatZH": "调整防火墙规则顺序",
"formatEN": "reorder firewall rule"
},
"/hosts/firewall/rules/reset": {
"bodyKeys": [],
@@ -3746,25 +3470,21 @@
"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": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新防火墙规则",
"formatEN": "update firewall rule"
},
"/hosts/firewall/settings/ipv6": {
"bodyKeys": [
"uuid"
"status"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新防火墙规则 [uuid]",
"formatEN": "update firewall rule [uuid]"
"formatZH": "设置防火墙 IPv6 支持为 [status]",
"formatEN": "Set firewall IPv6 support to [status]"
},
"/hosts/firewall/settings/operate": {
"bodyKeys": [
@@ -3806,11 +3526,13 @@
"formatEN": "update firewall port whitelist"
},
"/hosts/monitor/clean": {
"bodyKeys": [],
"bodyKeys": [
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清空监控数据",
"formatEN": "clean monitor datas"
"formatZH": "清空 [type] 监控数据",
"formatEN": "clean [type] monitoring data"
},
"/hosts/monitor/setting/update": {
"bodyKeys": [
@@ -4028,12 +3750,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": [
@@ -5489,6 +5220,282 @@
"formatZH": "更新 vLLM [name]",
"formatEN": "update vLLM [name]"
},
"/xpack/vms": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/xpack/vms/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "virtual_machines",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/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"
},
"/xpack/vms/iso": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/xpack/vms/iso/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_isos",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/xpack/vms/iso/update": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/xpack/vms/networks": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/xpack/vms/networks/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/xpack/vms/networks/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/xpack/vms/operate": {
"bodyKeys": [
"name",
"operate"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/xpack/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/xpack/vms/rename": {
"bodyKeys": [
"name",
"newName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/xpack/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/xpack/vms/storages": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/xpack/vms/storages/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/xpack/vms/storages/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/xpack/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/xpack/vms/templates": {
"bodyKeys": [
"name",
"templateName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/xpack/vms/templates/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_templates",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/xpack/vms/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机 [name]",
"formatEN": "update VM [name]"
},
"/xpack/waf/block/export": {
"bodyKeys": [],
"paramKeys": [],
+2
View File
@@ -10,6 +10,8 @@ const (
AppOpenclaw = "openclaw"
AppCopaw = "copaw"
AppHermesAgent = "hermes-agent"
AppDeepSeekHarness = "deepseek-harness"
AppOctop = "octop"
AppMysql = "mysql"
AppMariaDB = "mariadb"
AppPostgresql = "postgresql"
+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
+28 -30
View File
@@ -1,18 +1,16 @@
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.27.3
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/cli v29.9.0+incompatible
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-connections v0.8.1
github.com/docker/go-connections v0.8.2
github.com/fsnotify/fsnotify v1.10.1
github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0
@@ -29,10 +27,11 @@ require (
github.com/jinzhu/copier v0.4.0
github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.19.2
github.com/mattn/go-shellwords v1.0.14
github.com/mattn/go-shellwords v1.0.16
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
@@ -41,7 +40,7 @@ require (
github.com/pkg/sftp v1.13.11
github.com/qiniu/go-sdk/v7 v7.27.0
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.26.7
github.com/shirou/gopsutil/v4 v4.26.9
github.com/sirupsen/logrus v1.10.2
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/afero v1.15.0
@@ -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
golang.org/x/net v0.58.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.41.0
golang.org/x/time v0.15.0
go.mongodb.org/mongo-driver/v2 v2.9.2
golang.org/x/crypto v0.58.0
golang.org/x/net v0.60.0
golang.org/x/sync v0.24.0
golang.org/x/sys v0.49.0
golang.org/x/text v0.43.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
@@ -113,7 +112,7 @@ require (
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.2 // indirect
github.com/ebitengine/purego v0.11.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect
@@ -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
@@ -180,7 +177,7 @@ require (
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
@@ -206,21 +203,22 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect
go.mongodb.org/mongo-driver v1.17.10 // 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/image v0.45.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
+86 -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=
@@ -230,12 +230,18 @@ github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.8.1+incompatible h1:qYL1bCp6cRw2SB1xmLlIOPyV171dilw9W2Jew38vy9c=
github.com/docker/cli v29.8.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.9.0+incompatible h1:Cw9x50ioSGgGwXJ4eO+hIBry/w3LLbyF8GPAEKz1fnE=
github.com/docker/cli v29.9.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
@@ -250,6 +256,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.11.1 h1:2zpWRSQNVKN4eKsKO9eM1ILDgWfYMY9GwqRmK6XeQ/0=
github.com/ebitengine/purego v0.11.1/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -307,8 +315,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 +446,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=
@@ -566,6 +575,8 @@ github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-shellwords v1.0.14 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg=
github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-shellwords v1.0.16 h1:RRxAaRzU1YbzOSCj9NJqg2/VIbSWv0dnPoD3EwE8kxI=
github.com/mattn/go-shellwords v1.0.16/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -597,16 +608,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=
@@ -702,6 +713,8 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU=
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
@@ -751,6 +764,10 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCw
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
github.com/shirou/gopsutil/v4 v4.26.8 h1:YQMTF/1J50B5+Y0vlo1eDRf5DoR7Gk69hY+8wjYkQeo=
github.com/shirou/gopsutil/v4 v4.26.8/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
github.com/shirou/gopsutil/v4 v4.26.9 h1:CaBo/hBFqvlJoLvAQEKdHdf5GD97/MN2ACfyv63+2Ig=
github.com/shirou/gopsutil/v4 v4.26.9/go.mod h1:nKH+8wX2zxr/mDbsR+AAy6Qd2z4ZOQ7wfQaEp+yrYmg=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -877,8 +894,14 @@ 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 v1.17.10 h1:kdAgQvu8TROXZpSkJQd5wzfaNCCrMbpZyKFtQ6qkPCE=
go.mongodb.org/mongo-driver v1.17.10/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
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.mongodb.org/mongo-driver/v2 v2.9.1 h1:jewiFs2m1/VOQp8qhFshX6hWZ+EAXDhZHXExAUMcOgQ=
go.mongodb.org/mongo-driver/v2 v2.9.1/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA=
go.mongodb.org/mongo-driver/v2 v2.9.2 h1:OCAdjI37pC/SVxucz/9CM8zD4jCdegtsY5QcKzYFCgE=
go.mongodb.org/mongo-driver/v2 v2.9.2/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 +912,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 +974,12 @@ 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/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
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=
@@ -969,6 +996,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -996,6 +1025,7 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1052,6 +1082,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1076,8 +1108,10 @@ 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/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.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=
@@ -1155,6 +1189,10 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1170,6 +1208,8 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1188,13 +1228,17 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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=
@@ -1251,6 +1295,8 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1315,10 +1361,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 +1382,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=
+62 -19
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Unable to derive a project name from Docker Compose
ErrComposeNameInvalid: 'Invalid Compose project name: it must start with a lowercase letter or number, contain only lowercase letters, numbers, hyphens, and underscores, and be 1-256 characters long.'
ErrAppVersionUnavailable: 'This application version has been removed from the remote service. Please select another version and try again.'
ErrVllmGB10ProOnly: 'This vLLM version is available only in 1Panel Professional Edition.'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway can only upgrade within the same database type (SQLite or PostgreSQL).'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'The AI Gateway backup and target application must use the same database type (SQLite or PostgreSQL).'
ErrHermesUpgradeVersion: 'Hermes Agent can only upgrade to a newer recognized release allowed by its cross-version upgrade policy. Restore an older version from a backup instead.'
ErrAppWarn: 'App status abnormal; check logs'
ErrAppParamKey: 'Invalid app parameter: {{ .name }}'
ErrAppUpgrade: 'App upgrade failed: {{ .name }} {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'Build Cache'
Volume: 'Volume'
Network: "Network"
PruneStart: 'Cleaning in progress'
NetworkCleanupConnected: "Skipped network [{{ .name }}] ({{ .id }}): containers connected or network in use; not deleted"
NetworkCleanupSummary: "Network cleanup finished: deleted {{ .deleted }}, skipped {{ .skipped }}, failed {{ .failed }}"
NetworkCleanupPartialFailure: "Some networks could not be cleaned; see the task log"
ContainerBackupPrepare: 'Prepare container backup workspace'
ContainerBackupStop: 'Stop container before backup'
ContainerBackupInspect: 'Backup container inspect and network'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'Application type or name does not match'
# alert
ErrAlertMethodNotSupported: 'This version does not support this alert method'
ErrAlertSync: 'Failed to sync alert message'
ErrWebhookTestFailed: 'Please check the Webhook configuration. See the system logs for details.'
# task - runtime
ErrInstallExtension: 'Another extension install task is running'
# alert mail template
PanelAlertTitle: "Panel Alert Notification"
TestAlertTitle: "Test Email - Verify Email Connectivity"
TestAlert: "This is a test email to verify that your email sending configuration is correct."
TestAlertTitle: "Panel Test Notification"
TestAlertMessage: "This is a test notification to verify the notification channel configuration and connectivity."
LicenseExpirationAlert: "Panel {{ .node }}{{ .ip }}: license expires in {{ .day }} days. Log in to view details."
CronJobFailedAlert: "Panel {{ .node }}{{ .ip }}: scheduled task {{ .name }} failed. Log in to view details."
ClamAlert: "Panel {{ .node }}{{ .ip }}: virus scan found {{ .num }} infected files. Log in to view details."
@@ -649,6 +656,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 +690,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 +704,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 +720,56 @@ 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."
ErrIPv6RARisk: "IPv6 forwarding is blocked to prevent possible network loss. See the documentation and system logs for details."
ErrIPv6RACheckFailed: "IPv6 forwarding is blocked because the network configuration could not be checked. See the system logs for details."
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 }}]"
+62 -19
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'No se pudo derivar un nombre de proyecto desde la c
ErrComposeNameInvalid: 'Nombre de proyecto Compose no válido: debe comenzar con una letra minúscula o un número, contener solo letras minúsculas, números, guiones y guiones bajos, y tener entre 1 y 256 caracteres.'
ErrAppVersionUnavailable: 'Esta versión de la aplicación se ha eliminado del servicio remoto. Seleccione otra versión e inténtelo de nuevo.'
ErrVllmGB10ProOnly: 'Esta versión de vLLM solo está disponible en 1Panel Professional Edition.'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway solo puede actualizarse dentro del mismo tipo de base de datos (SQLite o PostgreSQL).'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'La copia de seguridad de AI Gateway y la aplicación de destino deben usar el mismo tipo de base de datos (SQLite o PostgreSQL).'
ErrHermesUpgradeVersion: 'Hermes Agent solo puede actualizarse a una versión reconocida más reciente permitida por la configuración de actualización entre versiones principales. Para volver a una versión anterior, restaure una copia de seguridad.'
ErrAppWarn: 'Estado anómalo, revise el log'
ErrAppParamKey: 'El campo de parámetro {{ .name }} es anómalo'
ErrAppUpgrade: 'La actualización de la aplicación {{ .name }} falló {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'Caché de compilación'
Volume: 'Volumen de almacenamiento'
Network: 'Red'
PruneStart: 'Limpiando en progreso espere...'
NetworkCleanupConnected: "Se omitió la red [{{ .name }}] ({{ .id }}): hay contenedores conectados o la red está en uso; no se eliminó"
NetworkCleanupSummary: "Limpieza de redes finalizada: eliminadas {{ .deleted }}, omitidas {{ .skipped }}, fallidas {{ .failed }}"
NetworkCleanupPartialFailure: "No se pudieron limpiar algunas redes; consulte el registro de la tarea"
ContainerBackupPrepare: 'Preparar espacio de respaldo del contenedor'
ContainerBackupStop: 'Detener contenedor antes del respaldo'
ContainerBackupInspect: 'Respaldar inspect y red del contenedor'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'El tipo o nombre de la aplicación no coincide'
# alerta
ErrAlertMethodNotSupported: 'Esta versión no admite este método de alerta'
ErrAlertSync: 'Error en la sincronización de la alerta, revise e intente de nuevo'
ErrWebhookTestFailed: 'Revisa la configuración del Webhook. Consulta los registros del sistema para más detalles.'
# tarea - entorno de ejecución
ErrInstallExtension: 'Ya hay una tarea de instalación en curso, espere a que termine'
# plantilla de correo de alerta
PanelAlertTitle: 'Notificación de alerta del panel'
TestAlertTitle: 'Correo de prueba - Verificar conectividad de correo'
TestAlert: 'Este es un correo de prueba para verificar que la configuración de envío de correos es correcta.'
TestAlertTitle: 'Notificación de prueba del panel'
TestAlertMessage: 'Esta es una notificación de prueba para verificar la configuración y la conectividad del canal de notificaciones.'
LicenseExpirationAlert: 'Su Panel {{ .node }}{{ .ip }}, la licencia expirará en {{ .day }} días. Inicie sesión en el panel para ver los detalles.'
CronJobFailedAlert: 'Su Panel {{ .node }}{{ .ip }}, la ejecución de la tarea programada {{ .name }} falló. Inicie sesión en el panel para ver los detalles.'
ClamAlert: 'Su Panel {{ .node }}{{ .ip }}, la tarea de escaneo antivirus detectó {{ .num }} archivos infectados. Inicie sesión en el panel para ver los detalles.'
@@ -649,6 +656,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 +690,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 +704,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 +720,56 @@ 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."
ErrIPv6RARisk: "El reenvío IPv6 está bloqueado para evitar una posible pérdida de conectividad. Consulte la documentación y los registros del sistema."
ErrIPv6RACheckFailed: "El reenvío IPv6 está bloqueado porque no se pudo comprobar la configuración de red. Consulte los registros del sistema."
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 }}]"
+63 -19
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'نام پروژه از پیکربندی Docker Comp
ErrComposeNameInvalid: 'نام پروژه Compose نامعتبر است: باید با حرف کوچک یا عدد شروع شود، فقط شامل حروف کوچک، اعداد، خط تیره و زیرخط باشد و ۱ تا ۲۵۶ نویسه داشته باشد.'
ErrAppVersionUnavailable: 'این نسخه برنامه از سرویس راه دور حذف شده است. لطفاً نسخه دیگری را انتخاب کرده و دوباره تلاش کنید.'
ErrVllmGB10ProOnly: 'این نسخه vLLM فقط در نسخه حرفه‌ای 1Panel در دسترس است.'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway فقط در همان نوع پایگاه داده (SQLite یا PostgreSQL) قابل ارتقا است.'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'نسخه پشتیبان AI Gateway و برنامه مقصد باید از یک نوع پایگاه داده (SQLite یا PostgreSQL) استفاده کنند.'
ErrHermesUpgradeVersion: 'Hermes Agent فقط می‌تواند به نسخه جدیدتر و شناخته‌شده‌ای ارتقا یابد که تنظیمات ارتقا بین نسخه‌های اصلی اجازه می‌دهد. برای بازگشت به نسخه قدیمی، نسخه پشتیبان را بازیابی کنید.'
ErrAppWarn: 'وضعیت برنامه غیرعادی است؛ لاگ‌ها را بررسی کنید'
ErrAppParamKey: 'پارامتر برنامه نامعتبر است: {{ .name }}'
ErrAppUpgrade: 'ارتقاء برنامه ناموفق بود: {{ .name }} {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'کش ساخت'
Volume: 'حجم'
Network: "شبکه"
PruneStart: 'پاک‌سازی در حال انجام است'
NetworkCleanupConnected: "از شبکه [{{ .name }}] ({{ .id }}) صرف‌نظر شد: کانتینر متصل دارد یا در حال استفاده است؛ حذف نشد"
NetworkCleanupSummary: "پاک‌سازی شبکه‌ها پایان یافت: حذف‌شده {{ .deleted }}، ردشده {{ .skipped }}، ناموفق {{ .failed }}"
NetworkCleanupPartialFailure: "پاک‌سازی برخی شبکه‌ها ناموفق بود؛ گزارش وظیفه را بررسی کنید"
ContainerBackupPrepare: 'آماده‌سازی فضای کاری پشتیبان کانتینر'
ContainerBackupStop: 'توقف کانتینر قبل از پشتیبان‌گیری'
ContainerBackupInspect: 'بررسی و شبکه کانتینر پشتیبان'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'نوع یا نام برنامه مطابقت ندارد'
# هشدار
ErrAlertMethodNotSupported: 'این نسخه از این روش هشدار پشتیبانی نمی‌کند'
ErrAlertSync: 'همگام‌سازی پیام هشدار ناموفق بود'
ErrWebhookTestFailed: 'لطفاً پیکربندی وب‌هوک را بررسی کنید. برای جزئیات، گزارش‌های سیستم را ببینید.'
# وظیفه - محیط اجرا
ErrInstallExtension: 'وظیفه نصب پسوند دیگری در حال اجراست'
# قالب ایمیل هشدار
PanelAlertTitle: "اعلان هشدار پنل"
TestAlertTitle: "ایمیل آزمایشی - تأیید اتصال ایمیل"
TestAlert: "این یک ایمیل آزمایشی برای تأیید صحت پیکربندی ارسال ایمیل شما است."
TestAlertTitle: "اعلان آزمایشی پنل"
TestAlertMessage: "این یک اعلان آزمایشی برای بررسی پیکربندی و اتصال کانال اعلان است."
LicenseExpirationAlert: "پنل {{ .node }}{{ .ip }}: مجوز در {{ .day }} روز منقضی می‌شود. برای مشاهده جزئیات وارد شوید."
CronJobFailedAlert: "پنل {{ .node }}{{ .ip }}: وظیفه زمان‌بندی {{ .name }} ناموفق بود. برای مشاهده جزئیات وارد شوید."
ClamAlert: "پنل {{ .node }}{{ .ip }}: اسکن ویروس {{ .num }} فایل آلوده پیدا کرد. برای مشاهده جزئیات وارد شوید."
@@ -649,6 +656,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 +691,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 +705,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 +721,56 @@ 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 }} مشخص‌شده در قانون انتقال وجود ندارد. نام رابط را بررسی کرده و دوباره تلاش کنید."
ErrIPv6RARisk: "فعال‌سازی هدایت IPv6 به دلیل خطر قطع شبکه مسدود شد. مستندات و گزارش‌های سیستم را بررسی کنید."
ErrIPv6RACheckFailed: "فعال‌سازی هدایت IPv6 به دلیل عدم امکان بررسی پیکربندی شبکه مسدود شد. جزئیات در گزارش‌های سیستم موجود است."
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 }}]"
+62 -19
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Docker Compose 設定またはファイルの親デ
ErrComposeNameInvalid: 'Compose プロジェクト名の形式が正しくありません。小文字または数字で始まり、小文字、数字、ハイフン、アンダースコアのみを使用した 1~256 文字にしてください。'
ErrAppVersionUnavailable: '現在のアプリケーションバージョンはリモートサービスから削除されています。別のバージョンを選択して再試行してください。'
ErrVllmGB10ProOnly: 'この vLLM バージョンは 1Panel プロフェッショナル版でのみ利用できます。'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway は同じデータベース種別(SQLite または PostgreSQL)間でのみアップグレードできます。'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'AI Gateway のバックアップと復元先アプリのデータベース種別(SQLite または PostgreSQL)は一致している必要があります。'
ErrHermesUpgradeVersion: 'Hermes Agent は、認識可能でメジャーバージョン間のアップグレード設定に適合する新しいリリースにのみアップグレードできます。以前のバージョンに戻すには、バックアップから復元してください。'
ErrAppWarn: '異常な状態です。ログを確認してください'
ErrAppParamKey: 'パラメータ {{ .name }} フィールドが異常です'
ErrAppUpgrade: 'アプリケーション {{ .name }} のアップグレードに失敗しました {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'ビルドキャッシュ'
Volume: 'ストレージボリューム'
Network: 'ネットワーク'
PruneStart: 'クリーンアップ中、しばらくお待ちください...'
NetworkCleanupConnected: "ネットワーク [{{ .name }}] ({{ .id }}) をスキップしました:コンテナが接続されているか使用中のため、削除しませんでした"
NetworkCleanupSummary: "ネットワークのクリーンアップが完了しました:削除 {{ .deleted }} 件、スキップ {{ .skipped }} 件、失敗 {{ .failed }} 件"
NetworkCleanupPartialFailure: "一部のネットワークをクリーンアップできませんでした。タスクログを確認してください"
ContainerBackupPrepare: 'コンテナバックアップ作業領域を準備'
ContainerBackupStop: 'バックアップ前にコンテナを停止'
ContainerBackupInspect: 'コンテナ inspect とネットワーク情報をバックアップ'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'アプリケーションの種類または名前が一
# アラート
ErrAlertMethodNotSupported: 'このバージョンではこの通知方法は利用できません'
ErrAlertSync: 'アラーム情報の同期エラーです。確認してもう一度お試しください。'
ErrWebhookTestFailed: 'Webhook の設定を確認してください。詳細はシステムログを参照してください。'
# タスク - ランタイム
ErrInstallExtension: 'インストールタスクが進行中です、タスクが終了するのを待ってください'
# アラートメールテンプレート
PanelAlertTitle: 'パネルアラート通知'
TestAlertTitle: 'テストメール - メール接続の確認'
TestAlert: 'これはテストメールです。メール送信設定が正しく構成されているかを確認します。'
TestAlertTitle: 'パネルのテスト通知'
TestAlertMessage: 'これは通知チャネルの設定と接続性を確認するためのテスト通知です。'
LicenseExpirationAlert: 'あなたの {{ .node }}{{ .ip }} パネル、ライセンスは {{ .day }} 日後に期限切れになります。詳細はパネルにログインして'
CronJobFailedAlert: 'あなたの {{ .node }}{{ .ip }} パネル、スケジュールタスク {{ .name }} の実行に失敗しました。詳細はパネルにログインして'
ClamAlert: 'あなたの {{ .node }}{{ .ip }} パネル、ウイルススキャンタスクで {{ .num }} 個の感染ファイルを検出しました。詳細はパネルにログインして'
@@ -649,6 +656,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 +690,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 +704,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 +720,56 @@ 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 }} は存在しません。名前を確認して再試行してください。"
ErrIPv6RARisk: "ネットワーク切断のおそれがあるため、IPv6 転送の有効化をブロックしました。ドキュメントとシステムログをご確認ください。"
ErrIPv6RACheckFailed: "ネットワーク設定を確認できないため、IPv6 転送の有効化をブロックしました。詳細はシステムログをご確認ください。"
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 }}] を削除"

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