mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-14 08:00:39 +00:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
547f9775b0 | ||
|
|
f37afb1a37 | ||
|
|
3b89d83554 | ||
|
|
12bfe6a1bd | ||
|
|
aaf70cfc41 | ||
|
|
1ecd4f7f0c | ||
|
|
a8ccdcade7 | ||
|
|
4a89c0c031 | ||
|
|
f21ebb1a36 | ||
|
|
3011383b07 | ||
|
|
fd12866897 | ||
|
|
bc6cf519ce | ||
|
|
f9c43989fe | ||
|
|
e90b201001 | ||
|
|
f7394938f7 | ||
|
|
dada7294dd | ||
|
|
afcb94f6ef | ||
|
|
5a2e430dff | ||
|
|
f2ca4f56e6 | ||
|
|
b6f609aaee | ||
|
|
c960c0137d | ||
|
|
09a901294e | ||
|
|
4d481a8f31 | ||
|
|
f1e0aa3391 | ||
|
|
f475500543 | ||
|
|
8415a1ce28 | ||
|
|
faf4933a02 | ||
|
|
c692a9f5e3 | ||
|
|
e7f0c21715 | ||
|
|
da7e8f1dbf | ||
|
|
486cd5d0ad | ||
|
|
00ce5a7b36 | ||
|
|
c17f055e06 | ||
|
|
f22df4b29c | ||
|
|
b9bab90a14 |
@@ -10,6 +10,9 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/webhook_sender"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -378,10 +381,24 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Type == constant.Custom {
|
if req.Type == constant.Custom {
|
||||||
result, err := alertService.TestCustomAlertConfig(req)
|
lang := strings.TrimSpace(c.GetHeader("Accept-Language"))
|
||||||
|
if lang == "" {
|
||||||
|
lang = i18n.GetLanguageFromDB()
|
||||||
|
}
|
||||||
|
result, err := alertService.TestCustomAlertConfig(req, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
if global.LOG != nil {
|
||||||
return
|
global.LOG.Warnf("webhook test preparation failed: configID=%d error=%q", req.ID,
|
||||||
|
webhook_sender.DiagnosticText(err.Error(), webhook_sender.Request{
|
||||||
|
Format: webhook_sender.BodyJSON, Body: []byte(req.Config),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
result = dto.AlertConfigTestResult{Success: false}
|
||||||
|
}
|
||||||
|
result.Response = ""
|
||||||
|
result.Message = ""
|
||||||
|
if !result.Success {
|
||||||
|
result.Message = i18n.GetMsgByKeyAndLang(lang, "ErrWebhookTestFailed")
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, result)
|
helper.SuccessWithData(c, result)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
|
|||||||
|
|
||||||
// @Tags Database Redis
|
// @Tags Database Redis
|
||||||
// @Summary Install redis-cli
|
// @Summary Install redis-cli
|
||||||
// @Success 200
|
// @Accept json
|
||||||
|
// @Param request body dto.RedisCliInstall true "request"
|
||||||
|
// @Success 200 {string} string
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/redis/install/cli [post]
|
// @Router /databases/redis/install/cli [post]
|
||||||
func (b *BaseApi) InstallCli(c *gin.Context) {
|
func (b *BaseApi) InstallCli(c *gin.Context) {
|
||||||
if err := redisService.InstallCli(); err != nil {
|
var req dto.RedisCliInstall
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := redisService.InstallCli(req)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
helper.Success(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Database Redis
|
// @Tags Database Redis
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/rules/update [post]
|
// @Router /hosts/firewall/rules/update [post]
|
||||||
// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [instanceKey]","formatEN":"update firewall rule [instanceKey]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则","formatEN":"update firewall rule"}
|
||||||
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||||
var request dto.FirewallRuleUpdate
|
var request dto.FirewallRuleUpdate
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
@@ -340,7 +340,7 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/rules/reorder [post]
|
// @Router /hosts/firewall/rules/reorder [post]
|
||||||
// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [instanceKey]","formatEN":"reorder firewall rule [instanceKey]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序","formatEN":"reorder firewall rule"}
|
||||||
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
||||||
var request dto.FirewallRuleReorder
|
var request dto.FirewallRuleReorder
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
@@ -556,7 +556,7 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/docker/policies/delete/batch [post]
|
// @Router /hosts/firewall/docker/policies/delete/batch [post]
|
||||||
// @x-panel-log {"bodyKeys":["uuids"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略 [uuids]","formatEN":"delete Docker port guard policies [uuids]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略","formatEN":"delete Docker port guard policies"}
|
||||||
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
||||||
var request dto.DockerPortGuardPolicyBatchDelete
|
var request dto.DockerPortGuardPolicyBatchDelete
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := monitorService.LoadVLLMMonitorData(req)
|
data, err := monitorService.LoadVLLMMonitorData(c.Request.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
|
|||||||
Port int64 `json:"port"`
|
Port int64 `json:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedisCliInstall struct {
|
||||||
|
TaskID string `json:"taskID" validate:"omitempty,uuid"`
|
||||||
|
}
|
||||||
|
|
||||||
// mysql
|
// mysql
|
||||||
type MysqlDBSearch struct {
|
type MysqlDBSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ type FirewallBackendFamilyStatus struct {
|
|||||||
Initialized bool `json:"initialized"`
|
Initialized bool `json:"initialized"`
|
||||||
Bound bool `json:"bound"`
|
Bound bool `json:"bound"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
ForwardPolicy string `json:"forwardPolicy,omitempty"`
|
|
||||||
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ type GPUProcess struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitorVLLMSearch struct {
|
type MonitorVLLMSearch struct {
|
||||||
|
Group string `json:"group" validate:"omitempty,oneof=overview latency requests"`
|
||||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
StartTime time.Time `json:"startTime" validate:"required"`
|
StartTime time.Time `json:"startTime" validate:"required"`
|
||||||
EndTime time.Time `json:"endTime" validate:"required"`
|
EndTime time.Time `json:"endTime" validate:"required"`
|
||||||
|
|||||||
@@ -85,6 +85,14 @@ type MonitorVLLM struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
RawMetrics string `json:"-"`
|
RawMetrics string `json:"-"`
|
||||||
HistogramDeltas string `json:"-"`
|
HistogramDeltas string `json:"-"`
|
||||||
|
IntervalSeconds float64 `json:"intervalSeconds" gorm:"-"`
|
||||||
|
RequestDeltas string `json:"-"`
|
||||||
|
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"`
|
Running *float64 `json:"running"`
|
||||||
Waiting *float64 `json:"waiting"`
|
Waiting *float64 `json:"waiting"`
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -16,7 +17,6 @@ type VLLMMonitorRepo struct{}
|
|||||||
type VLLMHistoryPoint struct {
|
type VLLMHistoryPoint struct {
|
||||||
model.MonitorVLLM
|
model.MonitorVLLM
|
||||||
Bucket int64
|
Bucket int64
|
||||||
HistogramSamples string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
|
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
|
||||||
@@ -41,16 +41,30 @@ func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
|
|||||||
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
|
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *VLLMMonitorRepo) Count(id uint, start, end time.Time) (int64, error) {
|
func (r *VLLMMonitorRepo) Count(ctx context.Context, id uint, start, end time.Time) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
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
|
err := db.Count(&count).Error
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *VLLMMonitorRepo) History(id uint, start, end time.Time, seconds int64, aggregation string) ([]VLLMHistoryPoint, error) {
|
func (r *VLLMMonitorRepo) History(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string) ([]VLLMHistoryPoint, error) {
|
||||||
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||||
metrics := []string{"running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99"}
|
var 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
|
var columns []string
|
||||||
if seconds > 0 {
|
if seconds > 0 {
|
||||||
operation := "AVG"
|
operation := "AVG"
|
||||||
@@ -59,20 +73,52 @@ func (r *VLLMMonitorRepo) History(id uint, start, end time.Time, seconds int64,
|
|||||||
}
|
}
|
||||||
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
|
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
|
||||||
for _, column := range metrics {
|
for _, column := range metrics {
|
||||||
if aggregation != "max" && (strings.HasPrefix(column, "time_to_first_token_p") || strings.HasPrefix(column, "time_per_output_token_p") || strings.HasPrefix(column, "request_latency_p")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
columns = append(columns, operation+"("+column+") AS "+column)
|
columns = append(columns, operation+"("+column+") AS "+column)
|
||||||
}
|
}
|
||||||
if aggregation != "max" {
|
|
||||||
columns = append(columns, "json_group_array(json(NULLIF(histogram_deltas, ''))) AS histogram_samples")
|
|
||||||
}
|
|
||||||
db = db.Group("bucket").Order("bucket ASC")
|
db = db.Group("bucket").Order("bucket ASC")
|
||||||
} else {
|
} else {
|
||||||
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
|
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
|
||||||
|
if group == "" || group == "requests" {
|
||||||
|
columns = append(columns, "histogram_deltas", "request_deltas")
|
||||||
|
}
|
||||||
db = db.Order("created_at ASC, id ASC")
|
db = db.Order("created_at ASC, id ASC")
|
||||||
}
|
}
|
||||||
var points []VLLMHistoryPoint
|
var points []VLLMHistoryPoint
|
||||||
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
|
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
|
||||||
return points, err
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type ISettingRepo interface {
|
|||||||
GetList(opts ...DBOption) ([]model.Setting, error)
|
GetList(opts ...DBOption) ([]model.Setting, error)
|
||||||
Get(opts ...DBOption) (model.Setting, error)
|
Get(opts ...DBOption) (model.Setting, error)
|
||||||
GetValueByKey(key string) (string, error)
|
GetValueByKey(key string) (string, error)
|
||||||
|
GetCoreValueByKey(key string) (string, error)
|
||||||
Create(key, value string) error
|
Create(key, value string) error
|
||||||
Update(key, value string) error
|
Update(key, value string) error
|
||||||
WithByKey(key string) DBOption
|
WithByKey(key string) DBOption
|
||||||
@@ -74,6 +75,17 @@ func (s *SettingRepo) GetValueByKey(key string) (string, error) {
|
|||||||
return setting.Value, nil
|
return setting.Value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) GetCoreValueByKey(key string) (string, error) {
|
||||||
|
if global.CoreDB == nil {
|
||||||
|
return "", errors.New("core database is unavailable")
|
||||||
|
}
|
||||||
|
var setting model.Setting
|
||||||
|
if err := global.CoreDB.Where("key = ?", key).First(&setting).Error; err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return setting.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingRepo) WithByKey(key string) DBOption {
|
func (s *SettingRepo) WithByKey(key string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("key = ?", key)
|
return g.Where("key = ?", key)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
@@ -110,7 +111,9 @@ func (t TaskRepo) Update(ctx context.Context, task *model.Task) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t TaskRepo) UpdateRunningTaskToFailed() error {
|
func (t TaskRepo) UpdateRunningTaskToFailed() error {
|
||||||
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{"status": constant.StatusFailed, "error_msg": "1Panel restart causes failure"}).Error
|
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{
|
||||||
|
"status": constant.StatusFailed, "error_msg": constant.InterruptedMsg, "end_at": time.Now(),
|
||||||
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TaskRepo) CountExecutingTask() (int64, error) {
|
func (t TaskRepo) CountExecutingTask() (int64, error) {
|
||||||
|
|||||||
@@ -652,7 +652,7 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
|||||||
if len(ignores) > 0 {
|
if len(ignores) > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if common.IsCrossVersion(install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
if isCrossAppVersion(install.App.Key, install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
versions = append(versions, item.Version)
|
versions = append(versions, item.Version)
|
||||||
@@ -660,12 +660,12 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
|||||||
if len(versions) == 0 {
|
if len(versions) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
versions = common.GetSortedVersions(versions)
|
versions = sortAppVersions(install.App.Key, versions)
|
||||||
lastVersion := versions[0]
|
lastVersion := versions[0]
|
||||||
if common.IsCrossVersion(install.Version, lastVersion) {
|
if isCrossAppVersion(install.App.Key, install.Version, lastVersion) {
|
||||||
return install.App.CrossVersionUpdate
|
return install.App.CrossVersionUpdate
|
||||||
}
|
}
|
||||||
return common.CompareVersion(lastVersion, install.Version)
|
return isNewerAppVersion(install.App.Key, lastVersion, install.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
type openclawConfig struct {
|
type openclawConfig struct {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareAIGatewayAppRecover(install *model.AppInstall, recoverFile, secret string) (string, func(), error) {
|
||||||
|
noop := func() {}
|
||||||
|
if !isAIGatewayApp(install.App.Key) {
|
||||||
|
return "", noop, nil
|
||||||
|
}
|
||||||
|
tmpDir, err := os.MkdirTemp(global.Dir.TmpDir, "ai-gateway-recover-")
|
||||||
|
if err != nil {
|
||||||
|
return "", noop, err
|
||||||
|
}
|
||||||
|
cleanup := func() { _ = os.RemoveAll(tmpDir) }
|
||||||
|
fail := func(err error) (string, func(), error) {
|
||||||
|
cleanup()
|
||||||
|
return "", noop, err
|
||||||
|
}
|
||||||
|
if err := files.NewFileOp().TarGzExtractPro(recoverFile, tmpDir, secret); err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
backupDir := filepath.Join(tmpDir, strings.ReplaceAll(filepath.Base(recoverFile), ".tar.gz", ""))
|
||||||
|
for _, name := range []string{"app.json", "app.tar.gz"} {
|
||||||
|
info, err := os.Lstat(filepath.Join(backupDir, name))
|
||||||
|
if err != nil || !info.Mode().IsRegular() {
|
||||||
|
return fail(buserr.New("AppBackupFileIncomplete"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata, err := os.ReadFile(filepath.Join(backupDir, "app.json"))
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
var backup model.AppInstall
|
||||||
|
if err := json.Unmarshal(metadata, &backup); err != nil {
|
||||||
|
return fail(fmt.Errorf("unmarshal app.json failed: %w", err))
|
||||||
|
}
|
||||||
|
if backup.App.Key != install.App.Key || backup.Name != install.Name {
|
||||||
|
return fail(buserr.New("AppAttributesNotMatch"))
|
||||||
|
}
|
||||||
|
// Historical backups already include Version. Unsuffixed releases are
|
||||||
|
// SQLite; no new metadata field is required. Missing versions are invalid,
|
||||||
|
// not evidence that an archive contains SQLite data.
|
||||||
|
if strings.TrimSpace(backup.Version) == "" || strings.TrimSpace(install.Version) == "" {
|
||||||
|
return fail(buserr.New("AppBackupFileIncomplete"))
|
||||||
|
}
|
||||||
|
// Deliberately do not compare version order: restoring an older version of
|
||||||
|
// the same database type is required for failed-upgrade rollbacks.
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(install.App.Key, install.Version, backup.Version) {
|
||||||
|
return fail(buserr.New("ErrAIGatewayRecoverDatabaseTypeMismatch"))
|
||||||
|
}
|
||||||
|
return backupDir, cleanup, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const aiGatewayAppKey = "ai-gateway"
|
||||||
|
|
||||||
|
func isAIGatewayApp(appKey string) bool {
|
||||||
|
return appKey == aiGatewayAppKey || appKey == "local"+aiGatewayAppKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAIGatewayPostgresVersion(version string) bool {
|
||||||
|
return strings.HasSuffix(version, "-pg") || strings.HasSuffix(version, "-postgres")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database variants are deployment choices, not interchangeable upgrades.
|
||||||
|
func isAIGatewayUpgradeVersionAllowed(appKey, currentVersion, targetVersion string) bool {
|
||||||
|
return !isAIGatewayApp(appKey) ||
|
||||||
|
isAIGatewayPostgresVersion(currentVersion) == isAIGatewayPostgresVersion(targetVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortAIGatewayVersions(versions []string) {
|
||||||
|
sort.SliceStable(versions, func(i, j int) bool {
|
||||||
|
if common.CompareVersion(versions[i], versions[j]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if common.CompareVersion(versions[j], versions[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Prefer SQLite only when the numeric versions are equal.
|
||||||
|
return !isAIGatewayPostgresVersion(versions[i]) && isAIGatewayPostgresVersion(versions[j])
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -77,7 +77,7 @@ type IAlertService interface {
|
|||||||
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
|
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
|
||||||
DeleteAlertConfig(id uint) error
|
DeleteAlertConfig(id uint) error
|
||||||
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
||||||
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
|
TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAlertService() IAlertService {
|
func NewIAlertService() IAlertService {
|
||||||
@@ -851,7 +851,7 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
|||||||
|
|
||||||
msg := email.EmailMessage{
|
msg := email.EmailMessage{
|
||||||
Subject: i18n.GetMsgByKey("TestAlertTitle"),
|
Subject: i18n.GetMsgByKey("TestAlertTitle"),
|
||||||
Body: i18n.GetMsgByKey("TestAlert"),
|
Body: i18n.GetMsgByKey("TestAlertMessage"),
|
||||||
IsHTML: false,
|
IsHTML: false,
|
||||||
}
|
}
|
||||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||||
@@ -899,7 +899,7 @@ func resolveEmailTestConfig(req dto.AlertConfigTest) (dto.AlertEmailConfig, erro
|
|||||||
return emailConfig, nil
|
return emailConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error) {
|
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error) {
|
||||||
if req.Type != constant.Custom {
|
if req.Type != constant.Custom {
|
||||||
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
|
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
|
||||||
}
|
}
|
||||||
@@ -928,12 +928,15 @@ func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertC
|
|||||||
}
|
}
|
||||||
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
|
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
if global.LOG != nil {
|
||||||
|
global.LOG.Warnf("webhook test failed: %v", providers.ErrCustomWebhookUnsupported)
|
||||||
|
}
|
||||||
return dto.AlertConfigTestResult{
|
return dto.AlertConfigTestResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return tester.TestCustomWebhook(resolved)
|
return tester.TestCustomWebhook(resolved, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
||||||
|
|||||||
@@ -550,6 +550,11 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
|
|
||||||
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
|
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
appInstall.Status = constant.StatusInstallErr
|
||||||
|
appInstall.Message = err.Error()
|
||||||
|
if saveErr := appInstallRepo.Save(context.Background(), appInstall); saveErr != nil {
|
||||||
|
err = fmt.Errorf("%w; save failed install status: %v", err, saveErr)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,7 +562,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
installApp := func(t *task.Task) error {
|
installApp := func(t *task.Task) (err error) {
|
||||||
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
|
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
install.Status = constant.StatusUpErr
|
||||||
|
install.Message = err.Error()
|
||||||
|
if saveErr := appInstallRepo.Save(context.Background(), &install); saveErr != nil {
|
||||||
|
return fmt.Errorf("%w; save failed operation status: %v", err, saveErr)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
operation := string(req.Operate)
|
operation := string(req.Operate)
|
||||||
@@ -567,11 +572,15 @@ func (a *AppInstallService) SyncAll(systemInit bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, i := range allList {
|
for _, i := range allList {
|
||||||
if i.Status == constant.StatusInstalling || i.Status == constant.StatusUpgrading || i.Status == constant.StatusRebuilding || i.Status == constant.StatusUninstalling {
|
if appInstallOperationInterruptedOnRestart(i.Status) {
|
||||||
if systemInit {
|
if systemInit {
|
||||||
i.Status = constant.StatusError
|
i.Status = appInstallOperationFailureStatus(i.Status)
|
||||||
i.Message = "1Panel restart causes the task to terminate"
|
i.Message = constant.InterruptedMsg
|
||||||
_ = appInstallRepo.Save(context.Background(), &i)
|
if err := appInstallRepo.Save(context.Background(), &i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err := syncAppInstallStatus(&i, false); err != nil {
|
||||||
|
global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -678,14 +687,17 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
if !canAccessVllmVersion(app.Key, detail.Version) {
|
if !canAccessVllmVersion(app.Key, detail.Version) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(app.Key, install.Version, detail.Version) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
||||||
if len(ignores) > 0 {
|
if len(ignores) > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if common.IsCrossVersion(install.Version, detail.Version) && !app.CrossVersionUpdate {
|
if isCrossAppVersion(app.Key, install.Version, detail.Version) && !app.CrossVersionUpdate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
canUpgrade := common.CompareVersion(detail.Version, install.Version)
|
canUpgrade := isNewerAppVersion(app.Key, detail.Version, install.Version)
|
||||||
if app.Key == vllmAppKeyForUpgrade {
|
if app.Key == vllmAppKeyForUpgrade {
|
||||||
currentImage := loadVllmImageFromEnv(install.Env)
|
currentImage := loadVllmImageFromEnv(install.Env)
|
||||||
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
|
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
|
||||||
@@ -726,7 +738,7 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Slice(versions, func(i, j int) bool {
|
sort.Slice(versions, func(i, j int) bool {
|
||||||
return common.CompareVersion(versions[i].Version, versions[j].Version)
|
return appVersionSortBefore(app.Key, versions[i].Version, versions[j].Version)
|
||||||
})
|
})
|
||||||
return versions, nil
|
return versions, nil
|
||||||
}
|
}
|
||||||
@@ -931,10 +943,34 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||||
|
operation := ""
|
||||||
|
switch appInstall.Status {
|
||||||
|
case constant.StatusInstalling:
|
||||||
|
operation = task.TaskInstall
|
||||||
|
case constant.StatusUpgrading:
|
||||||
|
operation = task.TaskUpgrade
|
||||||
|
case constant.StatusRebuilding:
|
||||||
|
operation = task.TaskBuild
|
||||||
|
case constant.StatusUninstalling:
|
||||||
|
operation = task.TaskUninstall
|
||||||
|
case constant.StatusStarting, constant.StatusWaiting:
|
||||||
|
operation = task.TaskUpdate
|
||||||
|
case constant.StatusRestarting:
|
||||||
|
operation = task.TaskRestart
|
||||||
|
}
|
||||||
|
if operation != "" {
|
||||||
|
if err := reconcileAppInstallTaskFailure(appInstall, operation, global.DB, global.TaskDB); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
switch appInstall.Status {
|
switch appInstall.Status {
|
||||||
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||||
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
||||||
return nil
|
return nil
|
||||||
|
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
|
||||||
|
if !force {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cli, err := docker.NewClient()
|
cli, err := docker.NewClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,6 +106,14 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(install.App.Key, install.Version, detail.Version) {
|
||||||
|
return buserr.New("ErrAIGatewayDatabaseTypeMismatch")
|
||||||
|
}
|
||||||
|
if isHermesApp(install.App.Key) &&
|
||||||
|
(!isNewerAppVersion(install.App.Key, detail.Version, install.Version) ||
|
||||||
|
(isCrossAppVersion(install.App.Key, install.Version, detail.Version) && !install.App.CrossVersionUpdate)) {
|
||||||
|
return buserr.New("ErrHermesUpgradeVersion")
|
||||||
|
}
|
||||||
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
|
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1406,9 +1406,8 @@ func handleErr(install model.AppInstall, err error, out string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doNotNeedSync(installed model.AppInstall) bool {
|
func doNotNeedSync(installed model.AppInstall) bool {
|
||||||
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading ||
|
return appInstallOperationPending(installed.Status) || installed.Status == constant.StatusSyncing ||
|
||||||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr ||
|
installed.Status == constant.StatusInstallErr || installed.Status == constant.StatusUpgradeErr || installed.Status == constant.StatusUpErr
|
||||||
installed.Status == constant.StatusStarting || installed.Status == constant.StatusRestarting || installed.Status == constant.StatusWaiting
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
|
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
|
||||||
@@ -1502,6 +1501,11 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, installed := range appInstallList {
|
for _, installed := range appInstallList {
|
||||||
|
if sync && appInstallOperationPending(installed.Status) {
|
||||||
|
if err := syncAppInstallStatus(&installed, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if updated && ignoreUpdate(installed) {
|
if updated && ignoreUpdate(installed) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1567,11 +1571,14 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
|
|
||||||
var versions []string
|
var versions []string
|
||||||
for _, appDetail := range details {
|
for _, appDetail := range details {
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(installed.App.Key, installed.Version, appDetail.Version) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(appDetail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(appDetail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
||||||
if len(ignores) > 0 {
|
if len(ignores) > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if common.IsCrossVersion(installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate {
|
if isCrossAppVersion(installed.App.Key, installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
versions = append(versions, appDetail.Version)
|
versions = append(versions, appDetail.Version)
|
||||||
@@ -1586,7 +1593,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
versions = common.GetSortedVersions(versions)
|
versions = sortAppVersions(installed.App.Key, versions)
|
||||||
lastVersion := versions[0]
|
lastVersion := versions[0]
|
||||||
|
|
||||||
if installed.App.Key == constant.AppMysql || installed.App.Key == constant.AppMysqlCluster {
|
if installed.App.Key == constant.AppMysql || installed.App.Key == constant.AppMysqlCluster {
|
||||||
@@ -1601,10 +1608,10 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if common.IsCrossVersion(installed.Version, lastVersion) {
|
if isCrossAppVersion(installed.App.Key, installed.Version, lastVersion) {
|
||||||
installDTO.CanUpdate = installed.App.CrossVersionUpdate
|
installDTO.CanUpdate = installed.App.CrossVersionUpdate
|
||||||
} else {
|
} else {
|
||||||
installDTO.CanUpdate = common.CompareVersion(lastVersion, installed.Version)
|
installDTO.CanUpdate = isNewerAppVersion(installed.App.Key, lastVersion, installed.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
if updated {
|
if updated {
|
||||||
@@ -2275,7 +2282,12 @@ func getAppVersions(key string, details []model.AppDetail) []string {
|
|||||||
}
|
}
|
||||||
versionsRaw = append(versionsRaw, detail.Version)
|
versionsRaw = append(versionsRaw, detail.Version)
|
||||||
}
|
}
|
||||||
versions = common.GetSortedVersions(versionsRaw)
|
if isAIGatewayApp(key) {
|
||||||
|
sortAIGatewayVersions(versionsRaw)
|
||||||
|
versions = versionsRaw
|
||||||
|
} else {
|
||||||
|
versions = sortAppVersions(key, versionsRaw)
|
||||||
|
}
|
||||||
if hasLatest {
|
if hasLatest {
|
||||||
versions = append([]string{latestVersion}, versions...)
|
versions = append([]string{latestVersion}, versions...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,11 +132,9 @@ func (u *BackupService) AppRecover(req dto.CommonRecover) error {
|
|||||||
if !fileOp.Stat(req.File) {
|
if !fileOp.Stat(req.File) {
|
||||||
return buserr.WithName("ErrFileNotFound", req.File)
|
return buserr.WithName("ErrFileNotFound", req.File)
|
||||||
}
|
}
|
||||||
if _, err := compose.Down(install.GetComposePath()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := handleAppRecover(&install, nil, req.File, false, req.Secret, req.TaskID); err != nil {
|
if err := handleAppRecover(&install, nil, req.File, false, req.Secret, req.TaskID); err != nil {
|
||||||
global.LOG.Errorf("recover app %s failed, err: %v", req.DetailName, err)
|
global.LOG.Errorf("recover app %s failed, err: %v", req.DetailName, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -200,8 +198,20 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error {
|
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error {
|
||||||
|
// Validate AI Gateway backups before stopping the app, creating rollback
|
||||||
|
// backups, or touching its files/database. Keep the validated extraction for
|
||||||
|
// the restore so uploaded/encrypted archives are not unpacked twice.
|
||||||
|
preparedPath, cleanup, err := prepareAIGatewayAppRecover(install, recoverFile, secret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if parentTask == nil && !isRollback {
|
||||||
|
if _, err := compose.Down(install.GetComposePath()); err != nil {
|
||||||
|
cleanup()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
var (
|
var (
|
||||||
err error
|
|
||||||
recoverTask *task.Task
|
recoverTask *task.Task
|
||||||
isOk = false
|
isOk = false
|
||||||
rollbackFile string
|
rollbackFile string
|
||||||
@@ -210,11 +220,13 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
if parentTask == nil {
|
if parentTask == nil {
|
||||||
recoverTask, err = task.NewTaskWithOps(install.Name, task.TaskRecover, task.TaskScopeBackup, taskID, install.ID)
|
recoverTask, err = task.NewTaskWithOps(install.Name, task.TaskRecover, task.TaskScopeBackup, taskID, install.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
cleanup()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recoverApp := func(t *task.Task) error {
|
recoverApp := func(t *task.Task) error {
|
||||||
|
defer cleanup()
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
if !isRollback {
|
if !isRollback {
|
||||||
rollbackFile = path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)))
|
rollbackFile = path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)))
|
||||||
@@ -223,10 +235,13 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tmpPath := preparedPath
|
||||||
|
if tmpPath == "" {
|
||||||
if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil {
|
if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "")
|
tmpPath = strings.ReplaceAll(recoverFile, ".tar.gz", "")
|
||||||
|
}
|
||||||
if err := fileOp.TarGzExtractPro(tmpPath+"/app.tar.gz", tmpPath, ""); err != nil {
|
if err := fileOp.TarGzExtractPro(tmpPath+"/app.tar.gz", tmpPath, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -236,7 +251,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
} else {
|
} else {
|
||||||
_, _ = compose.Up(install.GetComposePath())
|
_, _ = compose.Up(install.GetComposePath())
|
||||||
}
|
}
|
||||||
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
|
_ = os.RemoveAll(tmpPath)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") {
|
if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") {
|
||||||
@@ -397,6 +412,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
}
|
}
|
||||||
recoverTask.AddSubTask(task.GetTaskName(install.Name, task.TaskRecover, task.TaskScopeBackup), recoverApp, rollBackApp)
|
recoverTask.AddSubTask(task.GetTaskName(install.Name, task.TaskRecover, task.TaskScopeBackup), recoverApp, rollBackApp)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer cleanup()
|
||||||
_ = recoverTask.Execute()
|
_ = recoverTask.Execute()
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -371,43 +371,39 @@ func loadComposePathAndFiles(composeName string, dockerClient *client.Client) (s
|
|||||||
}
|
}
|
||||||
config := targetContainer.Labels[composeConfigLabel]
|
config := targetContainer.Labels[composeConfigLabel]
|
||||||
workdir := targetContainer.Labels[composeWorkdirLabel]
|
workdir := targetContainer.Labels[composeWorkdirLabel]
|
||||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
files, err := resolveComposeFiles(workdir, config)
|
||||||
composePath = config
|
if err != nil {
|
||||||
} else {
|
return "", nil, err
|
||||||
composePath = workdir
|
|
||||||
}
|
}
|
||||||
|
composePath = strings.Join(files, ",")
|
||||||
}
|
}
|
||||||
composeFiles := normalizeComposeFiles(composePath)
|
composeFiles, err := normalizeComposeFiles(composePath)
|
||||||
if len(composeFiles) == 0 {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("compose file not found for %s", composeName)
|
return "", nil, err
|
||||||
}
|
}
|
||||||
return composePath, composeFiles, nil
|
return strings.Join(composeFiles, ","), composeFiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeComposeFiles(composePath string) []string {
|
func normalizeComposeFiles(composePath string) ([]string, error) {
|
||||||
items := strings.Split(composePath, ",")
|
files, err := resolveComposeFiles("", composePath)
|
||||||
result := make([]string, 0)
|
if err != nil {
|
||||||
seen := make(map[string]struct{})
|
return nil, err
|
||||||
for _, item := range items {
|
|
||||||
item = strings.TrimSpace(item)
|
|
||||||
if item == "" {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
stat, err := os.Stat(item)
|
for i, file := range files {
|
||||||
if err == nil && stat.IsDir() {
|
info, err := os.Stat(file)
|
||||||
item = path.Join(item, "docker-compose.yml")
|
if err == nil && info.IsDir() {
|
||||||
|
file = filepath.Join(file, "docker-compose.yml")
|
||||||
|
info, err = os.Stat(file)
|
||||||
|
files[i] = file
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(item); err != nil {
|
if err != nil {
|
||||||
continue
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, ok := seen[item]; ok {
|
if !info.Mode().IsRegular() {
|
||||||
continue
|
return nil, fmt.Errorf("compose path %s is not a regular file", file)
|
||||||
}
|
}
|
||||||
seen[item] = struct{}{}
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
}
|
||||||
sort.Strings(result)
|
return files, nil
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *composeBackupContext) close() {
|
func (c *composeBackupContext) close() {
|
||||||
@@ -621,20 +617,46 @@ func safeJoinWithinBase(baseDir, name string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||||
if recoverCtx.targetDir != "" {
|
composeFiles := recoverCtx.meta.Files
|
||||||
_ = os.RemoveAll(recoverCtx.targetDir)
|
if strings.TrimSpace(recoverCtx.meta.ComposePath) != "" {
|
||||||
|
filesByPath := make(map[string]composeBackupFile, len(composeFiles))
|
||||||
|
for _, item := range composeFiles {
|
||||||
|
filesByPath[filepath.Clean(item.OriginalPath)] = item
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
|
orderedFiles := make([]composeBackupFile, 0, len(composeFiles))
|
||||||
return err
|
for _, file := range strings.Split(recoverCtx.meta.ComposePath, ",") {
|
||||||
|
file = strings.TrimSpace(file)
|
||||||
|
if file == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
restored := make([]string, 0, len(recoverCtx.meta.Files))
|
file = filepath.Clean(file)
|
||||||
for _, item := range recoverCtx.meta.Files {
|
item, ok := filesByPath[file]
|
||||||
|
if !ok {
|
||||||
|
if len(composeFiles) != 1 || file != filepath.Dir(composeFiles[0].OriginalPath) {
|
||||||
|
return fmt.Errorf("compose file %s is missing from backup metadata", file)
|
||||||
|
}
|
||||||
|
item = composeFiles[0]
|
||||||
|
}
|
||||||
|
orderedFiles = append(orderedFiles, item)
|
||||||
|
}
|
||||||
|
composeFiles = orderedFiles
|
||||||
|
}
|
||||||
|
if len(composeFiles) == 0 {
|
||||||
|
return fmt.Errorf("compose files are missing from backup metadata")
|
||||||
|
}
|
||||||
|
restored := make([]string, 0, len(composeFiles))
|
||||||
|
sourceByTarget := make(map[string]string, len(composeFiles))
|
||||||
|
for _, item := range composeFiles {
|
||||||
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
||||||
}
|
}
|
||||||
if !recoverCtx.fileOp.Stat(backupPath) {
|
info, err := os.Stat(backupPath)
|
||||||
continue
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return fmt.Errorf("compose backup path %s is not a regular file", backupPath)
|
||||||
}
|
}
|
||||||
targetName := item.FileName
|
targetName := item.FileName
|
||||||
if item.RelativePath != "" {
|
if item.RelativePath != "" {
|
||||||
@@ -650,6 +672,29 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
|
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
|
||||||
}
|
}
|
||||||
|
sourcePath := item.OriginalPath
|
||||||
|
if sourcePath == "" {
|
||||||
|
sourcePath = backupPath
|
||||||
|
}
|
||||||
|
sourcePath = filepath.Clean(sourcePath)
|
||||||
|
if previous, ok := sourceByTarget[targetPath]; ok && previous != sourcePath {
|
||||||
|
return fmt.Errorf("compose files %q and %q resolve to the same restore path %q", previous, sourcePath, targetPath)
|
||||||
|
}
|
||||||
|
sourceByTarget[targetPath] = sourcePath
|
||||||
|
restored = append(restored, targetPath)
|
||||||
|
}
|
||||||
|
if recoverCtx.targetDir != "" {
|
||||||
|
_ = os.RemoveAll(recoverCtx.targetDir)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, item := range composeFiles {
|
||||||
|
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
||||||
|
}
|
||||||
|
targetPath := restored[i]
|
||||||
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
|
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -660,7 +705,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
|||||||
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
|
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
restored = append(restored, targetPath)
|
|
||||||
}
|
}
|
||||||
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
|
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
|
||||||
if recoverCtx.fileOp.Stat(envPath) {
|
if recoverCtx.fileOp.Stat(envPath) {
|
||||||
@@ -672,14 +716,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(restored) == 0 {
|
|
||||||
defaultPath := path.Join(recoverCtx.targetDir, "docker-compose.yml")
|
|
||||||
if !recoverCtx.fileOp.Stat(defaultPath) {
|
|
||||||
return fmt.Errorf("compose file not found in backup data")
|
|
||||||
}
|
|
||||||
restored = append(restored, defaultPath)
|
|
||||||
}
|
|
||||||
sort.Strings(restored)
|
|
||||||
recoverCtx.composePath = strings.Join(restored, ",")
|
recoverCtx.composePath = strings.Join(restored, ",")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,40 +326,47 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
|||||||
case "container":
|
case "container":
|
||||||
inspectInfo, err = client.ContainerInspect(context.Background(), req.ID)
|
inspectInfo, err = client.ContainerInspect(context.Background(), req.ID)
|
||||||
case "compose":
|
case "compose":
|
||||||
filePath := ""
|
config, workdir := "", ""
|
||||||
cli, err := docker.NewDockerClient()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer cli.Close()
|
|
||||||
options := container.ListOptions{All: true}
|
options := container.ListOptions{All: true}
|
||||||
options.Filters = filters.NewArgs()
|
options.Filters = filters.NewArgs()
|
||||||
options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.ID))
|
options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.ID))
|
||||||
containers, err := cli.ContainerList(context.Background(), options)
|
containers, err := client.ContainerList(context.Background(), options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
for _, container := range containers {
|
|
||||||
config := container.Labels[composeConfigLabel]
|
|
||||||
if len(req.Detail) != 0 && strings.Contains(config, req.Detail) {
|
|
||||||
config = req.Detail
|
|
||||||
}
|
|
||||||
workdir := container.Labels[composeWorkdirLabel]
|
|
||||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
|
||||||
filePath = config
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
filePath = workdir
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(containers) == 0 {
|
if len(containers) == 0 {
|
||||||
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.ID))
|
composeItem, err := composeRepo.GetRecord(repo.WithByName(req.ID))
|
||||||
filePath = composeItem.Path
|
if err != nil {
|
||||||
}
|
|
||||||
if _, err := os.Stat(filePath); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
config = composeItem.Path
|
||||||
|
workdir = filepath.Dir(primaryComposePath(config))
|
||||||
|
} else {
|
||||||
|
config = containers[0].Labels[composeConfigLabel]
|
||||||
|
workdir = containers[0].Labels[composeWorkdirLabel]
|
||||||
|
}
|
||||||
|
composeFiles, err := resolveComposeFiles(workdir, config)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
filePath := composeFiles[0]
|
||||||
|
if strings.TrimSpace(req.Detail) != "" {
|
||||||
|
selectedFiles, err := resolveComposeFiles(workdir, req.Detail)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(selectedFiles) != 1 || !slices.Contains(composeFiles, selectedFiles[0]) {
|
||||||
|
return "", fmt.Errorf("compose file %s does not belong to project %s", req.Detail, req.ID)
|
||||||
|
}
|
||||||
|
filePath = selectedFiles[0]
|
||||||
|
}
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return "", fmt.Errorf("compose path %s is not a regular file", filePath)
|
||||||
|
}
|
||||||
content, err := os.ReadFile(filePath)
|
content, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -383,11 +390,6 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
||||||
client, err := docker.NewDockerClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
name := ""
|
name := ""
|
||||||
switch req.PruneType {
|
switch req.PruneType {
|
||||||
case "container":
|
case "container":
|
||||||
@@ -408,6 +410,14 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
|
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
|
||||||
|
if err := t.TaskCtx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
pruneFilters := filters.NewArgs()
|
pruneFilters := filters.NewArgs()
|
||||||
if req.WithTagAll {
|
if req.WithTagAll {
|
||||||
pruneFilters.Add("dangling", "false")
|
pruneFilters.Add("dangling", "false")
|
||||||
@@ -431,10 +441,7 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
|||||||
}
|
}
|
||||||
SpaceReclaimed = int(rep.SpaceReclaimed)
|
SpaceReclaimed = int(rep.SpaceReclaimed)
|
||||||
case "network":
|
case "network":
|
||||||
_, err := client.NetworksPrune(context.Background(), pruneFilters)
|
return cleanUnusedNetworks(t, client)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "volume":
|
case "volume":
|
||||||
versions, err := client.ServerVersion(context.Background())
|
versions, err := client.ServerVersion(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -803,33 +810,33 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
|
|||||||
func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) {
|
func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) {
|
||||||
messageChan := make(chan string, 1024)
|
messageChan := make(chan string, 1024)
|
||||||
errorChan := make(chan error, 1)
|
errorChan := make(chan error, 1)
|
||||||
doneChan := make(chan struct{})
|
logCtx, cancel := context.WithCancel(ctx.Request.Context())
|
||||||
|
defer cancel()
|
||||||
go func() {
|
doneChan := logCtx.Done()
|
||||||
<-ctx.Request.Context().Done()
|
|
||||||
close(doneChan)
|
|
||||||
}()
|
|
||||||
go collectLogs(doneChan, params, messageChan, errorChan)
|
go collectLogs(doneChan, params, messageChan, errorChan)
|
||||||
ctx.Stream(func(w io.Writer) bool {
|
ctx.Stream(func(w io.Writer) bool {
|
||||||
select {
|
return writeContainerLogMessage(w, doneChan, messageChan, errorChan)
|
||||||
case msg, ok := <-messageChan:
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, err := fmt.Fprintf(w, "data: %s\n\n", msg)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
case err := <-errorChan:
|
|
||||||
if err != nil {
|
|
||||||
_, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err.Error())
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeContainerLogMessage(w io.Writer, done <-chan struct{}, messages <-chan string, errors <-chan error) bool {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return false
|
||||||
|
case message, ok := <-messages:
|
||||||
|
if ok {
|
||||||
|
_, err := fmt.Fprintf(w, "data: %s\n\n", message)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
// The collector closes both channels when it finishes. Drain all buffered
|
||||||
|
// logs before handling its result, or a ready error channel can drop the tail.
|
||||||
|
if err := <-errors; err != nil {
|
||||||
|
_, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<- string, errorChan chan<- error) {
|
func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<- string, errorChan chan<- error) {
|
||||||
defer close(messageChan)
|
defer close(messageChan)
|
||||||
defer close(errorChan)
|
defer close(errorChan)
|
||||||
|
|||||||
@@ -71,10 +71,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
ContainerCount: 0,
|
ContainerCount: 0,
|
||||||
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
|
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
|
||||||
ConfigFile: localItem.Path,
|
ConfigFile: localItem.Path,
|
||||||
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"),
|
Workdir: filepath.Dir(primaryComposePath(localItem.Path)),
|
||||||
}
|
}
|
||||||
composeItemLocal.CreatedBy = "1Panel"
|
composeItemLocal.CreatedBy = "1Panel"
|
||||||
composeItemLocal.Path = localItem.Path
|
|
||||||
composeLocalMap[localItem.Name] = composeItemLocal
|
composeLocalMap[localItem.Name] = composeItemLocal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,11 +111,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
if ok {
|
if ok {
|
||||||
composeItem.CreatedBy = createdBy
|
composeItem.CreatedBy = createdBy
|
||||||
}
|
}
|
||||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
|
||||||
composeItem.Path = config
|
|
||||||
} else {
|
|
||||||
composeItem.Path = workdir
|
|
||||||
}
|
|
||||||
for i := 0; i < len(composeCreatedByLocal); i++ {
|
for i := 0; i < len(composeCreatedByLocal); i++ {
|
||||||
if composeCreatedByLocal[i].Name == name {
|
if composeCreatedByLocal[i].Name == name {
|
||||||
composeItem.CreatedBy = "1Panel"
|
composeItem.CreatedBy = "1Panel"
|
||||||
@@ -147,7 +141,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
|
|
||||||
for key, value := range mergedMap {
|
for key, value := range mergedMap {
|
||||||
value.Name = key
|
value.Name = key
|
||||||
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
composeFiles, err := resolveComposeFiles(value.Workdir, value.ConfigFile)
|
||||||
|
value.Path = strings.Join(composeFiles, ",")
|
||||||
|
value.ComposeFileExists = err == nil && composeFileExists(composeFiles)
|
||||||
value.IsPinned = pinnedByName[key]
|
value.IsPinned = pinnedByName[key]
|
||||||
records = append(records, value)
|
records = append(records, value)
|
||||||
}
|
}
|
||||||
@@ -192,29 +188,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
return int64(total), listItem, nil
|
return int64(total), listItem, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeFileExists(workdir, configFile string) bool {
|
|
||||||
workdir = strings.TrimSpace(workdir)
|
|
||||||
configFile = strings.TrimSpace(configFile)
|
|
||||||
if configFile == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, item := range strings.Split(configFile, ",") {
|
|
||||||
file := strings.TrimSpace(item)
|
|
||||||
if file == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(file) && workdir != "" {
|
|
||||||
file = filepath.Join(workdir, file)
|
|
||||||
}
|
|
||||||
file = filepath.Clean(file)
|
|
||||||
info, err := os.Stat(file)
|
|
||||||
if err == nil && !info.IsDir() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||||
if err := validateComposeCreateName(req); err != nil {
|
if err := validateComposeCreateName(req); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -522,10 +495,6 @@ func isComposeProjectNameEmptyError(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||||
if len(req.Path) == 0 && req.Operation == "delete" {
|
|
||||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if cmd.CheckIllegal(req.Path, req.Operation) {
|
if cmd.CheckIllegal(req.Path, req.Operation) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
@@ -535,13 +504,19 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
|||||||
}
|
}
|
||||||
if req.WithFile {
|
if req.WithFile {
|
||||||
for _, item := range strings.Split(req.Path, ",") {
|
for _, item := range strings.Split(req.Path, ",") {
|
||||||
if len(item) != 0 {
|
item = strings.TrimSpace(item)
|
||||||
_ = os.RemoveAll(path.Dir(item))
|
if !filepath.IsAbs(item) || filepath.Dir(item) == "/" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, err := os.Stat(item)
|
||||||
|
if err == nil && info.Mode().IsRegular() {
|
||||||
|
if err := os.RemoveAll(filepath.Dir(item)); err != nil && !req.Force {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
}
|
||||||
return nil
|
return composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||||
}
|
}
|
||||||
if req.Operation == "up" {
|
if req.Operation == "up" {
|
||||||
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
||||||
@@ -584,13 +559,13 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
|||||||
write.Flush()
|
write.Flush()
|
||||||
|
|
||||||
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.DetailPath)
|
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.DetailPath)
|
||||||
if err := newComposeEnv(req.DetailPath, req.Env); err != nil {
|
if err := newComposeEnv(primaryComposePath(req.Path), req.Env); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
|
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
|
||||||
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
||||||
if err := recreateCompose(string(oldFile), req.Path, req.Name); err != nil {
|
if err := recreateCompose(string(oldFile), req.Path, req.DetailPath, req.Name); err != nil {
|
||||||
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
||||||
@@ -702,9 +677,18 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func removeContainerForCompose(composeName, composePath string) error {
|
func removeContainerForCompose(composeName, composePath string) error {
|
||||||
if _, err := os.Stat(composePath); err == nil {
|
composeFiles, err := resolveComposeFiles("", composePath)
|
||||||
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil {
|
filesAvailable := err == nil
|
||||||
return errors.New(stdout)
|
for _, file := range composeFiles {
|
||||||
|
info, err := os.Stat(file)
|
||||||
|
if err != nil || !info.Mode().IsRegular() {
|
||||||
|
filesAvailable = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filesAvailable {
|
||||||
|
if stdout, err := compose.Operate(strings.Join(composeFiles, ","), "down", composeName); err != nil {
|
||||||
|
return fmt.Errorf("docker-compose down failed, std: %s, err: %w", stdout, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -722,14 +706,17 @@ func removeContainerForCompose(composeName, composePath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var removeErrors []error
|
||||||
for _, c := range containers {
|
for _, c := range containers {
|
||||||
_ = client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
if err := client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}); err != nil {
|
||||||
|
removeErrors = append(removeErrors, fmt.Errorf("remove container %s failed: %w", c.ID, err))
|
||||||
}
|
}
|
||||||
return nil
|
}
|
||||||
|
return errors.Join(removeErrors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func recreateCompose(content, path, projectName string) error {
|
func recreateCompose(content, composePath, detailPath, projectName string) error {
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
file, err := os.OpenFile(detailPath, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -738,7 +725,7 @@ func recreateCompose(content, path, projectName string) error {
|
|||||||
_, _ = write.WriteString(content)
|
_, _ = write.WriteString(content)
|
||||||
write.Flush()
|
write.Flush()
|
||||||
|
|
||||||
if stdout, err := compose.Up(path, projectName); err != nil {
|
if stdout, err := compose.Up(composePath, projectName); err != nil {
|
||||||
return errors.New(string(stdout))
|
return errors.New(string(stdout))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -746,6 +733,9 @@ func recreateCompose(content, path, projectName string) error {
|
|||||||
|
|
||||||
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
||||||
for i := 0; i < len(list); i++ {
|
for i := 0; i < len(list); i++ {
|
||||||
|
if list[i].Path == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
tmpPath := list[i].Path
|
tmpPath := list[i].Path
|
||||||
if strings.Contains(list[i].Path, ",") {
|
if strings.Contains(list[i].Path, ",") {
|
||||||
tmpPath = strings.Split(list[i].Path, ",")[0]
|
tmpPath = strings.Split(list[i].Path, ",")[0]
|
||||||
@@ -774,3 +764,35 @@ func newComposeEnv(pathItem string, env string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveComposeFiles(workdir, configFile string) ([]string, error) {
|
||||||
|
workdir = strings.TrimSpace(workdir)
|
||||||
|
var files []string
|
||||||
|
for _, item := range strings.Split(configFile, ",") {
|
||||||
|
file := strings.TrimSpace(item)
|
||||||
|
if file == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(file) {
|
||||||
|
if !filepath.IsAbs(workdir) {
|
||||||
|
return nil, fmt.Errorf("cannot resolve compose file %s without an absolute working directory", file)
|
||||||
|
}
|
||||||
|
file = filepath.Join(workdir, file)
|
||||||
|
}
|
||||||
|
files = append(files, filepath.Clean(file))
|
||||||
|
}
|
||||||
|
if len(files) == 0 {
|
||||||
|
return nil, fmt.Errorf("compose config file is not specified")
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeFileExists(files []string) bool {
|
||||||
|
for _, file := range files {
|
||||||
|
info, err := os.Stat(file)
|
||||||
|
if err == nil && info.Mode().IsRegular() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,16 +2,24 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var networkCleanupSlot = make(chan struct{}, 1)
|
||||||
|
|
||||||
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
|
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -172,3 +180,114 @@ func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanUnusedNetworks(t *task.Task, cli *client.Client) error {
|
||||||
|
ctx := t.TaskCtx
|
||||||
|
select {
|
||||||
|
case networkCleanupSlot <- struct{}{}:
|
||||||
|
defer func() { <-networkCleanupSlot }()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
networks, err := cli.NetworkList(ctx, network.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
deleted, skipped, failed := 0, 0, 0
|
||||||
|
defer func() {
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
|
||||||
|
"deleted": deleted, "skipped": skipped, "failed": failed,
|
||||||
|
}))
|
||||||
|
}()
|
||||||
|
var used map[string]bool
|
||||||
|
sort.Slice(networks, func(i, j int) bool { return networks[i].Name < networks[j].Name })
|
||||||
|
for _, n := range networks {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch n.Name {
|
||||||
|
case "none", "host", "bridge", "1panel-network":
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n.Scope != "local" || n.Ingress || n.ConfigOnly {
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values := map[string]interface{}{"name": n.Name, "id": n.ID}
|
||||||
|
if used == nil {
|
||||||
|
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
used = make(map[string]bool)
|
||||||
|
for _, c := range containers {
|
||||||
|
if c.NetworkSettings == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for name, endpoint := range c.NetworkSettings.Networks {
|
||||||
|
used[name] = true
|
||||||
|
if endpoint != nil {
|
||||||
|
used[endpoint.NetworkID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if used[n.Name] || used[n.ID] {
|
||||||
|
skipped++
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if errdefs.IsNotFound(err) {
|
||||||
|
skipped++
|
||||||
|
} else {
|
||||||
|
failed++
|
||||||
|
t.Logf("Failed to inspect network [%s] (%s): %v", n.Name, n.ID, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(inspected.Containers) > 0 {
|
||||||
|
skipped++
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case errdefs.IsNotFound(err):
|
||||||
|
skipped++
|
||||||
|
case errdefs.IsConflict(err):
|
||||||
|
skipped++
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||||
|
default:
|
||||||
|
failed++
|
||||||
|
t.Logf("Failed to remove network [%s] (%s): %v", n.Name, n.ID, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if failed > 0 {
|
||||||
|
return errors.New(i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
@@ -17,12 +23,15 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||||
"github.com/docker/docker/api/types/container"
|
dockerclient "github.com/docker/docker/client"
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RedisService struct{}
|
type RedisService struct{}
|
||||||
|
|
||||||
|
// The CLI container is shared by all remote Redis databases on this node.
|
||||||
|
var redisCliInstallMutex sync.Mutex
|
||||||
|
|
||||||
type IRedisService interface {
|
type IRedisService interface {
|
||||||
UpdateConf(req dto.RedisConfUpdate) error
|
UpdateConf(req dto.RedisConfUpdate) error
|
||||||
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
|
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
|
||||||
@@ -33,7 +42,7 @@ type IRedisService interface {
|
|||||||
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
|
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
|
||||||
|
|
||||||
CheckHasCli() bool
|
CheckHasCli() bool
|
||||||
InstallCli() error
|
InstallCli(req dto.RedisCliInstall) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIRedisService() IRedisService {
|
func NewIRedisService() IRedisService {
|
||||||
@@ -61,30 +70,65 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *RedisService) CheckHasCli() bool {
|
func (u *RedisService) CheckHasCli() bool {
|
||||||
client, err := docker.NewDockerClient()
|
installed, _ := u.checkCliInstalled()
|
||||||
if err != nil {
|
return installed
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
containerLists, err := client.ContainerList(context.Background(), container.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, item := range containerLists {
|
|
||||||
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *RedisService) InstallCli() error {
|
func (u *RedisService) checkCliInstalled() (bool, error) {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
info, err := client.ContainerInspect(context.Background(), "1Panel-redis-cli-tools")
|
||||||
|
if dockerclient.IsErrNotFound(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return info.ContainerJSONBase != nil && info.State != nil && info.State.Running, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *RedisService) InstallCli(req dto.RedisCliInstall) (string, error) {
|
||||||
|
if !redisCliInstallMutex.TryLock() {
|
||||||
|
return "", buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
defer redisCliInstallMutex.Unlock()
|
||||||
item := dto.ContainerOperate{
|
item := dto.ContainerOperate{
|
||||||
|
TaskID: req.TaskID,
|
||||||
Name: "1Panel-redis-cli-tools",
|
Name: "1Panel-redis-cli-tools",
|
||||||
Image: "redis:7.4.4",
|
Image: "redis:7.4.4",
|
||||||
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
|
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
|
||||||
}
|
}
|
||||||
return NewIContainerService().ContainerCreate(item, false)
|
running, err := taskRepo.GetFirst(
|
||||||
|
repo.WithByName(task.GetTaskName(item.Name, task.TaskCreate, task.TaskScopeContainer)),
|
||||||
|
repo.WithByType(task.TaskScopeContainer),
|
||||||
|
taskRepo.WithByStatus(constant.StatusExecuting),
|
||||||
|
)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if running.ID != "" {
|
||||||
|
return running.ID, nil
|
||||||
|
}
|
||||||
|
installed, err := u.checkCliInstalled()
|
||||||
|
if err != nil || installed {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if item.TaskID == "" {
|
||||||
|
item.TaskID = uuid.NewString()
|
||||||
|
}
|
||||||
|
if _, err := taskRepo.GetFirst(taskRepo.WithByID(item.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "", buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
if err := NewIContainerService().ContainerCreate(item, true); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return item.TaskID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
|
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
|
||||||
|
|||||||
@@ -108,9 +108,7 @@ func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystem
|
|||||||
status.Reason = constant.FirewallBackendNotInstalled
|
status.Reason = constant.FirewallBackendNotInstalled
|
||||||
return status, nil
|
return status, nil
|
||||||
}
|
}
|
||||||
status.IsExist = true
|
return status, err
|
||||||
status.Message = err.Error()
|
|
||||||
return status, nil
|
|
||||||
}
|
}
|
||||||
status.IsExist = true
|
status.IsExist = true
|
||||||
runtimeStatus, err := lifecycle.LoadStatus(client)
|
runtimeStatus, err := lifecycle.LoadStatus(client)
|
||||||
|
|||||||
@@ -80,6 +80,50 @@ func LoadPanelPort() string {
|
|||||||
return portSetting.Value
|
return portSetting.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SyncPanelPortWhitelist() error {
|
||||||
|
firewallWhitelistMu.Lock()
|
||||||
|
defer firewallWhitelistMu.Unlock()
|
||||||
|
portValue := global.CONF.Base.Port
|
||||||
|
if global.IsMaster {
|
||||||
|
var err error
|
||||||
|
portValue, err = settingRepo.GetCoreValueByKey("ServerPort")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
port, err := strconv.ParseUint(portValue, 10, 16)
|
||||||
|
if err != nil || port == 0 {
|
||||||
|
return fmt.Errorf("invalid panel port %q", portValue)
|
||||||
|
}
|
||||||
|
portValue = strconv.FormatUint(port, 10)
|
||||||
|
value, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var entries []firewall.PortWhitelist
|
||||||
|
if err := json.Unmarshal([]byte(value), &entries); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
changed := false
|
||||||
|
for index := range entries {
|
||||||
|
if entries[index].Type == firewall.PortWhitelistTypePanel && entries[index].Port != portValue {
|
||||||
|
entries[index].Port = portValue
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
updated, err := json.Marshal(entries)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return settingRepo.Update(constant.FirewallPortWhiteList, string(updated))
|
||||||
|
}
|
||||||
|
|
||||||
func (s *FirewallService) updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error {
|
func (s *FirewallService) updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error {
|
||||||
if len(ports) == 0 {
|
if len(ports) == 0 {
|
||||||
return fmt.Errorf("firewall whitelist %s requires a port", serviceType)
|
return fmt.Errorf("firewall whitelist %s requires a port", serviceType)
|
||||||
@@ -582,6 +626,18 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
importPrioritySupported := true
|
||||||
|
if adapter, ok := runtime.(*filterfirewalld.Adapter); ok && slices.ContainsFunc(request.Items, func(item dto.FirewallRuleCreateItem) bool {
|
||||||
|
return item.SourceKind == constant.FirewallRuleSourceImported
|
||||||
|
}) {
|
||||||
|
importPrioritySupported, err = adapter.SupportsRichRulePriority(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
if !importPrioritySupported && t != nil {
|
||||||
|
t.Log("firewalld does not support rich rule priority; ignoring priority for imported rules, original rule ordering is not preserved")
|
||||||
|
}
|
||||||
|
}
|
||||||
preparedItems := make([]*createItem, 0, len(request.Items))
|
preparedItems := make([]*createItem, 0, len(request.Items))
|
||||||
var stop error
|
var stop error
|
||||||
for index, item := range request.Items {
|
for index, item := range request.Items {
|
||||||
@@ -593,6 +649,16 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item.Rule.OrderIndex = nil
|
item.Rule.OrderIndex = nil
|
||||||
|
if item.SourceKind == constant.FirewallRuleSourceImported && !importPrioritySupported {
|
||||||
|
item.Rule.Priority = nil
|
||||||
|
if item.Rule.Scope.Provider == filter.ProviderFirewalld && item.Raw != "" && item.ParseStatus != "" && item.ParseStatus != filter.ParseStatusSupported && item.Rule.NativeKind != filter.NativeKindZoneService {
|
||||||
|
item.Raw, err = filterfirewalld.RemoveRichRulePriority(item.Raw)
|
||||||
|
if err != nil {
|
||||||
|
record(createItem{index: index, request: item}, "skipped", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
rules, err := expandFirewallCreateRule(item, selected)
|
rules, err := expandFirewallCreateRule(item, selected)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := "failed"
|
status := "failed"
|
||||||
@@ -693,7 +759,7 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR
|
|||||||
record(*item, "skipped", buserr.New("ErrRecordExist"))
|
record(*item, "skipped", buserr.New("ErrRecordExist"))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if converted {
|
if converted && importPrioritySupported {
|
||||||
priority++
|
priority++
|
||||||
}
|
}
|
||||||
if existing[key] {
|
if existing[key] {
|
||||||
@@ -713,7 +779,7 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR
|
|||||||
}
|
}
|
||||||
ufwActions[matchKey] = map[filter.Action]bool{item.request.Rule.Action: true}
|
ufwActions[matchKey] = map[filter.Action]bool{item.request.Rule.Action: true}
|
||||||
}
|
}
|
||||||
if converted {
|
if converted && importPrioritySupported {
|
||||||
if priority > 32767 {
|
if priority > 32767 {
|
||||||
return result, fmt.Errorf("%w: firewalld import order exceeds the priority range", filter.ErrExpansionLimit)
|
return result, fmt.Errorf("%w: firewalld import order exceeds the priority range", filter.ErrExpansionLimit)
|
||||||
}
|
}
|
||||||
@@ -729,18 +795,19 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item.request = prepared
|
item.request = prepared
|
||||||
actualKey, err := firewallRuleDuplicateKey(filter.ObservedRule{Rule: prepared.Rule, ParseStatus: prepared.ParseStatus}, false)
|
}
|
||||||
|
pending[key] = true
|
||||||
|
if converted {
|
||||||
|
actualKey, err := firewallRuleDuplicateKey(filter.ObservedRule{Rule: item.request.Rule, ParseStatus: item.request.ParseStatus}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
pending[key] = true
|
|
||||||
if existing[actualKey] || pending[actualKey] {
|
if existing[actualKey] || pending[actualKey] {
|
||||||
record(*item, "skipped", buserr.New("ErrRecordExist"))
|
record(*item, "skipped", buserr.New("ErrRecordExist"))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pending[actualKey] = true
|
pending[actualKey] = true
|
||||||
}
|
}
|
||||||
pending[key] = true
|
|
||||||
mergedItems = append(mergedItems, item)
|
mergedItems = append(mergedItems, item)
|
||||||
}
|
}
|
||||||
for _, item := range mergedItems {
|
for _, item := range mergedItems {
|
||||||
@@ -1185,15 +1252,17 @@ func loadForwardingFirewallOverview(manager forwarding.Adapter, families []strin
|
|||||||
}
|
}
|
||||||
ipv6, ipv6Err := loadForwardingFamilyInfo(manager, constant.FirewallFamilyIPv6)
|
ipv6, ipv6Err := loadForwardingFamilyInfo(manager, constant.FirewallFamilyIPv6)
|
||||||
if ipv6.Available {
|
if ipv6.Available {
|
||||||
interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile)
|
interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile, forwarding.LoadDockerIPv4BridgePorts)
|
||||||
var pathError *os.PathError
|
var pathError *os.PathError
|
||||||
switch {
|
switch {
|
||||||
case errors.As(err, &pathError) && errors.Is(err, os.ErrNotExist) && pathError.Path == "/proc/net/if_inet6":
|
case errors.As(err, &pathError) && errors.Is(err, os.ErrNotExist) && pathError.Path == "/proc/net/if_inet6":
|
||||||
ipv6.Available, ipv6.Initialized, ipv6.Bound = false, false, false
|
ipv6.Available, ipv6.Initialized, ipv6.Bound = false, false, false
|
||||||
case err != nil:
|
case err != nil:
|
||||||
|
global.LOG.Warnf("IPv6 forwarding RA check failed: %v", err)
|
||||||
ipv6.Reason = "ipv6_ra_check_failed"
|
ipv6.Reason = "ipv6_ra_check_failed"
|
||||||
ipv6.Bound = false
|
ipv6.Bound = false
|
||||||
case len(interfaces) > 0:
|
case len(interfaces) > 0:
|
||||||
|
global.LOG.Warnf("IPv6 forwarding RA risk: interfaces %s may depend on RA/SLAAC with accept_ra=1", strings.Join(interfaces, ", "))
|
||||||
ipv6.Reason, ipv6.RAInterfaces = "ipv6_ra_required", interfaces
|
ipv6.Reason, ipv6.RAInterfaces = "ipv6_ra_required", interfaces
|
||||||
ipv6.Bound = false
|
ipv6.Bound = false
|
||||||
case !ipv6.Bound:
|
case !ipv6.Bound:
|
||||||
|
|||||||
@@ -9,13 +9,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
@@ -82,23 +79,6 @@ func (s *ForwardingService) LoadBaseInfo(ctx context.Context) (dto.FirewallSubsy
|
|||||||
baseInfo.PingStatus = firewall.LoadPingStatus()
|
baseInfo.PingStatus = firewall.LoadPingStatus()
|
||||||
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
|
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
|
||||||
baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6
|
baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6
|
||||||
for _, family := range []struct {
|
|
||||||
command string
|
|
||||||
status *dto.FirewallBackendFamilyStatus
|
|
||||||
}{
|
|
||||||
{"iptables", &baseInfo.IPv4},
|
|
||||||
{"ip6tables", &baseInfo.IPv6},
|
|
||||||
} {
|
|
||||||
if family.command == "ip6tables" && !baseInfo.IPv6Enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
policy, err := loadForwardPolicy(ctx, family.command)
|
|
||||||
if err != nil {
|
|
||||||
global.LOG.Warnf("inspect %s FORWARD policy: %v", family.command, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
family.status.ForwardPolicy = policy
|
|
||||||
}
|
|
||||||
return baseInfo, nil
|
return baseInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,8 +262,6 @@ func (s *ForwardingService) Restore(ctx context.Context) error {
|
|||||||
if err := manager.OperateFamily(family, missing[family]); err != nil {
|
if err := manager.OperateFamily(family, missing[family]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for _, family := range families {
|
|
||||||
if !missing[family] {
|
if !missing[family] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -308,26 +286,3 @@ func (s *ForwardingService) Restore(ctx context.Context) error {
|
|||||||
func NewIForwardingService() IForwardingService {
|
func NewIForwardingService() IForwardingService {
|
||||||
return newForwardingService()
|
return newForwardingService()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadForwardPolicy(ctx context.Context, command string) (string, error) {
|
|
||||||
if !cmd.Which(command) {
|
|
||||||
command += "-nft"
|
|
||||||
if !cmd.Which(command) {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output, err := cmd.NewCommandMgr(cmd.WithContext(ctx), 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")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+30
-15
@@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
"github.com/moby/go-archive"
|
"github.com/moby/go-archive"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -325,18 +326,16 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
client, err := docker.NewDockerClient()
|
|
||||||
if err != nil {
|
|
||||||
taskItem.Log("Failed to create Docker client: " + err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
for _, itemPath := range req.Paths {
|
for _, itemPath := range req.Paths {
|
||||||
currentPath := itemPath
|
currentPath := itemPath
|
||||||
itemName := path.Base(currentPath)
|
itemName := path.Base(currentPath)
|
||||||
taskItem.AddSubTask(i18n.GetWithName("TaskImport", itemName), func(t *task.Task) error {
|
taskItem.AddSubTask(i18n.GetWithName("TaskImport", itemName), func(t *task.Task) error {
|
||||||
taskItem.Logf("----------------- %s -----------------", itemName)
|
taskItem.Logf("----------------- %s -----------------", itemName)
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
file, err := os.Open(currentPath)
|
file, err := os.Open(currentPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -347,14 +346,7 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
content, err := io.ReadAll(res.Body)
|
return consumeImageLoadResponse(res.Body, taskItem.Log)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if strings.Contains(string(content), "Error") {
|
|
||||||
return errors.New(string(content))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
_ = taskItem.Execute()
|
_ = taskItem.Execute()
|
||||||
@@ -362,6 +354,29 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Docker may report load failures in a successful HTTP response's JSON stream.
|
||||||
|
func consumeImageLoadResponse(reader io.Reader, log func(string)) error {
|
||||||
|
decoder := json.NewDecoder(reader)
|
||||||
|
for {
|
||||||
|
var message jsonmessage.JSONMessage
|
||||||
|
if err := decoder.Decode(&message); err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if message.Error != nil && message.Error.Message != "" {
|
||||||
|
return message.Error
|
||||||
|
}
|
||||||
|
if message.ErrorMessage != "" {
|
||||||
|
return errors.New(message.ErrorMessage)
|
||||||
|
}
|
||||||
|
if text := strings.TrimSpace(message.Stream); text != "" {
|
||||||
|
log(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ImageService) ImageSave(req dto.ImageSave) error {
|
func (u *ImageService) ImageSave(req dto.ImageSave) error {
|
||||||
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskExport, task.TaskScopeImage, req.TaskID, 1)
|
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskExport, task.TaskScopeImage, req.TaskID, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ type IMonitorService interface {
|
|||||||
|
|
||||||
LoadIOOptions() []string
|
LoadIOOptions() []string
|
||||||
LoadNetworkOptions() []string
|
LoadNetworkOptions() []string
|
||||||
LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
|
LoadVLLMMonitorData(ctx context.Context, req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
|
||||||
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error)
|
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error)
|
||||||
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
|
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
|
||||||
LoadGPUOptions() dto.MonitorGPUOptions
|
LoadGPUOptions() dto.MonitorGPUOptions
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ var vllmMetricsClient = &http.Client{
|
|||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
|
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) {
|
func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) {
|
||||||
data := dto.MonitorVLLMData{Points: []model.MonitorVLLM{}}
|
data := dto.MonitorVLLMData{Points: []model.MonitorVLLM{}}
|
||||||
if !req.EndTime.After(req.StartTime) {
|
if !req.EndTime.After(req.StartTime) {
|
||||||
return data, fmt.Errorf("invalid vLLM history request")
|
return data, fmt.Errorf("invalid vLLM history request")
|
||||||
}
|
}
|
||||||
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
|
install, err := appInstallRepo.GetFirstByCtx(ctx, repo.WithByID(req.AppInstallID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
@@ -49,23 +49,31 @@ func (m *MonitorService) LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.Mon
|
|||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
|
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
|
||||||
data.SampleCount, err = vllmMonitorRepo.Count(req.AppInstallID, req.StartTime, req.EndTime)
|
data.SampleCount, err = vllmMonitorRepo.Count(ctx, req.AppInstallID, req.StartTime, req.EndTime)
|
||||||
if err != nil || data.SampleCount == 0 {
|
if err != nil || data.SampleCount == 0 {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
if data.SampleCount > 1200 {
|
if data.SampleCount > 1200 {
|
||||||
data.BucketSeconds = (req.EndTime.Unix() - req.StartTime.Unix() + 600) / 600
|
data.BucketSeconds = (req.EndTime.Unix() - req.StartTime.Unix() + 600) / 600
|
||||||
}
|
}
|
||||||
points, err := vllmMonitorRepo.History(req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation)
|
points, err := vllmMonitorRepo.History(ctx, req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation, req.Group)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
if data.BucketSeconds > 0 {
|
if data.BucketSeconds > 0 {
|
||||||
if req.Aggregation != "max" {
|
aggregates := map[int64]*vllm.HistoryAccumulator{}
|
||||||
for i := range points {
|
err = vllmMonitorRepo.WalkDeltas(ctx, req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation, req.Group, func(bucket int64, histograms, requests string) error {
|
||||||
if err := vllm.AggregateHistograms(points[i].HistogramSamples, &points[i].MonitorVLLM); err != nil {
|
if aggregates[bucket] == nil {
|
||||||
|
aggregates[bucket] = &vllm.HistoryAccumulator{}
|
||||||
|
}
|
||||||
|
return aggregates[bucket].Add(histograms, requests, req.Aggregation, req.Group)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
for i := range points {
|
||||||
|
if aggregate := aggregates[points[i].Bucket]; aggregate != nil {
|
||||||
|
aggregate.Apply(&points[i].MonitorVLLM, req.Aggregation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next := 0
|
next := 0
|
||||||
@@ -93,6 +101,14 @@ func (m *MonitorService) LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.Mon
|
|||||||
}
|
}
|
||||||
interval := time.Duration(intervalSeconds) * time.Second
|
interval := time.Duration(intervalSeconds) * time.Second
|
||||||
for i, point := range points {
|
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 && point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
|
if i > 0 && point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
|
||||||
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: points[i-1].CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
|
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: points[i-1].CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -494,6 +494,9 @@ func restartCompose(composePath string, itemHelper *snapRecoverHelper) error {
|
|||||||
func recoverDBData(src string, itemHelper *snapRecoverHelper) error {
|
func recoverDBData(src string, itemHelper *snapRecoverHelper) error {
|
||||||
itemHelper.Task.Log("---------------------- 11 / 11 ----------------------")
|
itemHelper.Task.Log("---------------------- 11 / 11 ----------------------")
|
||||||
itemHelper.Task.LogStart(i18n.GetMsgByKey("RecoverDBData"))
|
itemHelper.Task.LogStart(i18n.GetMsgByKey("RecoverDBData"))
|
||||||
|
if err := os.Chmod(src, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
dbDir := path.Join(global.Dir.DataDir, "db")
|
dbDir := path.Join(global.Dir.DataDir, "db")
|
||||||
if err := os.RemoveAll(dbDir); err != nil {
|
if err := os.RemoveAll(dbDir); err != nil {
|
||||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("RecoverDBData"), err)
|
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("RecoverDBData"), err)
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error {
|
|||||||
taskItem.AddSubTask(
|
taskItem.AddSubTask(
|
||||||
i18n.GetWithName("SnapCopy", global.Dir.BaseDir),
|
i18n.GetWithName("SnapCopy", global.Dir.BaseDir),
|
||||||
func(t *task.Task) error {
|
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)
|
return FileOp.CopyDir(path.Join(rootDir, "1panel"), global.Dir.BaseDir)
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ func (u *TaskLogService) ReadByLine(req request.TaskLogReadReq) (*response.FileL
|
|||||||
if req.TaskID != "" {
|
if req.TaskID != "" {
|
||||||
opts = append(opts, taskRepo.WithByID(req.TaskID))
|
opts = append(opts, taskRepo.WithByID(req.TaskID))
|
||||||
} else {
|
} 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...)
|
taskModel, err := taskRepo.GetFirst(opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -526,10 +526,8 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
|||||||
website.SiteDir = create.SiteDir
|
website.SiteDir = create.SiteDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createdFiles := &websiteCreateFiles{}
|
||||||
configNginx := func(t *task.Task) error {
|
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 {
|
if create.Type == constant.Static && create.TemplateOutputID > 0 {
|
||||||
templateOutput, err := websiteTemplateOutputRepo.GetFirst(repo.WithByID(create.TemplateOutputID))
|
templateOutput, err := websiteTemplateOutputRepo.GetFirst(repo.WithByID(create.TemplateOutputID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -541,12 +539,15 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
|||||||
if _, err := os.Stat(templateOutput.OutputPath); err != nil {
|
if _, err := os.Stat(templateOutput.OutputPath); err != nil {
|
||||||
return buserr.New("ErrFileNotFound")
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig, initialSSL, createdFiles); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if website.Type != constant.Stream {
|
if website.Type != constant.Stream {
|
||||||
if err = createWafConfig(website, domains); err != nil {
|
if err = createWafConfig(website, domains, createdFiles); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if create.Type == constant.Runtime {
|
if create.Type == constant.Runtime {
|
||||||
@@ -555,10 +556,20 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceAppstore {
|
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()
|
tx, ctx := helper.GetTxAndContext()
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
@@ -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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteWebsite := func(t *task.Task) {
|
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)
|
createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
@@ -46,16 +45,7 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
|
|||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
if fileOp.Stat(wafDataPath) {
|
if fileOp.Stat(wafDataPath) {
|
||||||
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
|
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
|
||||||
content, err := fileOp.GetContent(websitesConfigPath)
|
if err := updateWAFWebsites(websitesConfigPath, func(websitesArray []request.WafWebsite) ([]request.WafWebsite, error) {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var websitesArray []request.WafWebsite
|
|
||||||
if content != nil {
|
|
||||||
if err := json.Unmarshal(content, &websitesArray); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for index, wafWebsite := range websitesArray {
|
for index, wafWebsite := range websitesArray {
|
||||||
if wafWebsite.Key == website.Alias {
|
if wafWebsite.Key == website.Alias {
|
||||||
wafSite := request.WafWebsite{
|
wafSite := request.WafWebsite{
|
||||||
@@ -74,11 +64,8 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
websitesContent, err := json.Marshal(websitesArray)
|
return websitesArray, nil
|
||||||
if err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,17 +122,8 @@ func (w WebsiteService) DeleteWebsiteDomain(domainId uint) error {
|
|||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
if fileOp.Stat(wafDataPath) {
|
if fileOp.Stat(wafDataPath) {
|
||||||
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
|
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
|
||||||
content, err := fileOp.GetContent(websitesConfigPath)
|
if err := updateWAFWebsites(websitesConfigPath, func(websitesArray []request.WafWebsite) ([]request.WafWebsite, error) {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var websitesArray []request.WafWebsite
|
|
||||||
var newWebsitesArray []request.WafWebsite
|
var newWebsitesArray []request.WafWebsite
|
||||||
if content != nil {
|
|
||||||
if err := json.Unmarshal(content, &websitesArray); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, wafWebsite := range websitesArray {
|
for _, wafWebsite := range websitesArray {
|
||||||
if wafWebsite.Key == website.Alias {
|
if wafWebsite.Key == website.Alias {
|
||||||
wafSite := wafWebsite
|
wafSite := wafWebsite
|
||||||
@@ -175,11 +153,8 @@ func (w WebsiteService) DeleteWebsiteDomain(domainId uint) error {
|
|||||||
newWebsitesArray = append(newWebsitesArray, wafWebsite)
|
newWebsitesArray = append(newWebsitesArray, wafWebsite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
websitesContent, err := json.Marshal(newWebsitesArray)
|
return newWebsitesArray, nil
|
||||||
if err != nil {
|
}); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,38 +403,3 @@ func renderMainHTMLFromZip(zipPath string, values map[string]string) (string, er
|
|||||||
}
|
}
|
||||||
return renderContent(string(content), values), nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
+308
-122
@@ -1,11 +1,13 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@@ -14,6 +16,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -124,7 +127,257 @@ func applyLocationProxyPass(location *components.Location, proxyPass string, sni
|
|||||||
location.UpdateDirective("proxy_ssl_name", []string{sslName})
|
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 (
|
var (
|
||||||
indexPath string
|
indexPath string
|
||||||
indexContent string
|
indexContent string
|
||||||
@@ -145,49 +398,23 @@ func createIndexFile(website *model.Website, runtime *model.Runtime) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileOp := files.NewFileOp()
|
if err := created.write(indexPath, []byte(indexContent), constant.FilePerm, true); err != nil {
|
||||||
if !fileOp.Stat(indexFolder) {
|
|
||||||
if err := fileOp.CreateDir(indexFolder, constant.DirPerm); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
html404, _ := websiteService.GetDefaultHtml("404")
|
||||||
if !fileOp.Stat(indexPath) {
|
if err := created.write(path.Join(indexFolder, "404.html"), []byte(html404.Content), constant.FilePerm, true); err != nil {
|
||||||
if err := fileOp.CreateFile(indexPath); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if website.Type == constant.Runtime && runtime.Resource == constant.ResourceAppstore {
|
if website.Type == constant.Runtime && runtime.Resource == constant.ResourceAppstore {
|
||||||
if err := chownRootDir(indexFolder); err != nil {
|
return created.chownNew(indexFolder)
|
||||||
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 nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createProxyFile(website *model.Website) error {
|
func createProxyFile(website *model.Website, created *websiteCreateFiles) error {
|
||||||
proxyFolder := GetSitePath(*website, SiteProxyDir)
|
proxyFolder := GetSitePath(*website, SiteProxyDir)
|
||||||
filePath := path.Join(proxyFolder, "root.conf")
|
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()
|
config, err := parser.NewStringParser(string(nginx_conf.Proxy)).Parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -201,57 +428,40 @@ func createProxyFile(website *model.Website) error {
|
|||||||
location.ChangePath("^~", "/")
|
location.ChangePath("^~", "/")
|
||||||
applyLocationProxyPass(location, website.Proxy, nil, "")
|
applyLocationProxyPass(location, website.Proxy, nil, "")
|
||||||
location.UpdateDirective("proxy_set_header", []string{"Host", "$host"})
|
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 buserr.WithErr("ErrUpdateBuWebsite", err)
|
||||||
}
|
}
|
||||||
return nil
|
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)
|
siteFolder := GetSiteDir(website.Alias)
|
||||||
fileOp := files.NewFileOp()
|
dirs := []string{siteFolder, path.Join(siteFolder, "log")}
|
||||||
if !fileOp.Stat(siteFolder) {
|
|
||||||
if err := fileOp.CreateDir(siteFolder, constant.DirPerm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := fileOp.CreateDir(path.Join(siteFolder, "log"), constant.DirPerm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := fileOp.CreateFile(path.Join(siteFolder, "log", "access.log")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := fileOp.CreateFile(path.Join(siteFolder, "log", "error.log")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if website.Type != constant.Stream {
|
if website.Type != constant.Stream {
|
||||||
if err := fileOp.CreateDir(path.Join(siteFolder, "index"), constant.DirPerm); err != nil {
|
dirs = append(dirs, path.Join(siteFolder, "index"), path.Join(siteFolder, "ssl"))
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if err := fileOp.CreateDir(path.Join(siteFolder, "ssl"), constant.DirPerm); err != nil {
|
for _, dir := range dirs {
|
||||||
|
if err := created.mkdir(dir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if website.Type == constant.Runtime {
|
for _, name := range []string{"access.log", "error.log"} {
|
||||||
if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceLocal {
|
if err := created.write(path.Join(siteFolder, "log", name), nil, constant.FilePerm, true); err != nil {
|
||||||
phpPoolDir := path.Join(siteFolder, "php-pool")
|
|
||||||
if err := fileOp.CreateDir(phpPoolDir, constant.DirPerm); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := fileOp.CreateFile(path.Join(phpPoolDir, "php-fpm.sock")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
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 website.Type == constant.Static || (website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP) {
|
if website.Type == constant.Static || (website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP) {
|
||||||
if err := createIndexFile(website, runtime); err != nil {
|
if err := createIndexFile(website, runtime, created); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if website.Type == constant.Proxy {
|
if website.Type == constant.Proxy {
|
||||||
if err := createProxyFile(website); err != nil {
|
return createProxyFile(website, created)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -261,12 +471,12 @@ type websiteInitialSSL struct {
|
|||||||
request request.WebsiteHTTPSOp
|
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)
|
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = createWebsiteFolder(website, runtime); err != nil {
|
if err = createWebsiteFolder(website, runtime, created); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
@@ -334,7 +544,10 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
|
|||||||
if initialSSL != nil {
|
if initialSSL != nil {
|
||||||
plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort)
|
plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort)
|
||||||
applyWebsiteSSLConfig(server, *website, plan, initialSSL.request)
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,18 +617,14 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.FilePath = configPath
|
config.FilePath = configPath
|
||||||
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
if err = created.write(configPath, []byte(nginx.DumpConfig(config, nginx.IndentedStyle)), constant.FilePerm, false); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
func moveDefaultWafConfig(websiteDir string, defaultConfigContent []byte, defaultRuleDir string, fileOp files.FileOp) error {
|
||||||
if !fileOp.Stat(websiteDir) {
|
if !fileOp.Stat(websiteDir) {
|
||||||
if err := fileOp.CreateDir(websiteDir, constant.DirPerm); err != nil {
|
if err := fileOp.CreateDir(websiteDir, constant.DirPerm); err != nil {
|
||||||
@@ -431,8 +640,7 @@ func moveDefaultWafConfig(websiteDir string, defaultConfigContent []byte, defaul
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defaultRulesName := []string{"acl", "args", "cookie", "defaultUaBlack", "defaultUrlBlack", "fileExt", "header", "methodWhite", "cdn"}
|
for _, ruleName := range defaultWAFRuleNames {
|
||||||
for _, ruleName := range defaultRulesName {
|
|
||||||
srcPath := path.Join(defaultRuleDir, ruleName+".json")
|
srcPath := path.Join(defaultRuleDir, ruleName+".json")
|
||||||
if fileOp.Stat(srcPath) {
|
if fileOp.Stat(srcPath) {
|
||||||
_ = fileOp.Copy(srcPath, websiteRuleDir)
|
_ = fileOp.Copy(srcPath, websiteRuleDir)
|
||||||
@@ -470,7 +678,10 @@ func createAllWebsitesWAFConfig(websites []model.Website) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
var (
|
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)
|
_ = 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)
|
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -510,16 +721,6 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
|
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{
|
wafWebsite := request.WafWebsite{
|
||||||
Key: website.Alias,
|
Key: website.Alias,
|
||||||
Domains: make([]string, 0),
|
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.Domains = append(wafWebsite.Domains, domain.Domain)
|
||||||
wafWebsite.Host = append(wafWebsite.Host, domain.Domain+":"+strconv.Itoa(domain.Port))
|
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 (
|
var (
|
||||||
sitesDir = path.Join(wafDataPath, "sites")
|
sitesDir = path.Join(wafDataPath, "sites")
|
||||||
@@ -551,20 +744,25 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
|
|||||||
return err
|
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 {
|
if err != nil {
|
||||||
_ = fileOp.DeleteDir(websiteDir)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := moveDefaultWafConfig(websiteDir, defaultConfigContent, defaultRuleDir, fileOp); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := created.write(path.Join(websiteDir, "rules", name+".json"), data, constant.FilePerm, false); err != nil {
|
||||||
if err = opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
|
}
|
||||||
|
if err := created.addWAFWebsite(websitesConfigPath, wafWebsite); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,27 +825,15 @@ func delWafConfig(website model.Website, force bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
|
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
|
||||||
content, err := fileOp.GetContent(websitesConfigPath)
|
if err := updateWAFWebsites(websitesConfigPath, func(websites []request.WafWebsite) ([]request.WafWebsite, error) {
|
||||||
if err != nil {
|
remaining := make([]request.WafWebsite, 0, len(websites))
|
||||||
return err
|
for _, existing := range websites {
|
||||||
}
|
if existing.Key != website.Alias {
|
||||||
var websitesArray []request.WafWebsite
|
remaining = append(remaining, existing)
|
||||||
var newWebsiteArray []request.WafWebsite
|
|
||||||
if len(content) > 0 {
|
|
||||||
if err = json.Unmarshal(content, &websitesArray); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, wafWebsite := range websitesArray {
|
return remaining, nil
|
||||||
if wafWebsite.Key != website.Alias {
|
}); err != nil {
|
||||||
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 err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,7 +258,15 @@ func (s *SubTask) Execute() error {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
|
s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
|
||||||
err = errors.New("timeout!")
|
err = errors.New("timeout!")
|
||||||
|
if s.RootTask.Task.Type == TaskScopeVm {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
case err = <-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 {
|
if err != nil {
|
||||||
s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", subTaskName, err))
|
s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", subTaskName, err))
|
||||||
if err.Error() == i18n.GetMsgByKey("ErrShutDown") {
|
if err.Error() == i18n.GetMsgByKey("ErrShutDown") {
|
||||||
|
|||||||
+348
-352
@@ -265,8 +265,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "应用端口修改 [key]-[name] => [port]",
|
"formatZH": "应用端口修改 [key]-[name] =\u003e [port]",
|
||||||
"formatEN": "Application port update [key]-[name] => [port]"
|
"formatEN": "Application port update [key]-[name] =\u003e [port]"
|
||||||
},
|
},
|
||||||
"/apps/installed/sort/update": {
|
"/apps/installed/sort/update": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -679,8 +679,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "容器重命名 [name] => [newName]",
|
"formatZH": "容器重命名 [name] =\u003e [newName]",
|
||||||
"formatEN": "rename container [name] => [newName]"
|
"formatEN": "rename container [name] =\u003e [newName]"
|
||||||
},
|
},
|
||||||
"/containers/repo": {
|
"/containers/repo": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -831,8 +831,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
"formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||||
"formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
"formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||||
},
|
},
|
||||||
"/core/auth/expired/reset": {
|
"/core/auth/expired/reset": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -1716,8 +1716,8 @@
|
|||||||
"output_value": "name"
|
"output_value": "name"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
"formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||||
"formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
"formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||||
},
|
},
|
||||||
"/core/enterprise/users/del": {
|
"/core/enterprise/users/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -1918,8 +1918,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]",
|
"formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]",
|
||||||
"formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]"
|
"formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]"
|
||||||
},
|
},
|
||||||
"/core/settings/memo": {
|
"/core/settings/memo": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -1948,8 +1948,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改系统端口 => [serverPort]",
|
"formatZH": "修改系统端口 =\u003e [serverPort]",
|
||||||
"formatEN": "update system port => [serverPort]"
|
"formatEN": "update system port =\u003e [serverPort]"
|
||||||
},
|
},
|
||||||
"/core/settings/proxy/update": {
|
"/core/settings/proxy/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -1974,8 +1974,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改系统 ssl => [ssl]",
|
"formatZH": "修改系统 ssl =\u003e [ssl]",
|
||||||
"formatEN": "update system ssl => [ssl]"
|
"formatEN": "update system ssl =\u003e [ssl]"
|
||||||
},
|
},
|
||||||
"/core/settings/terminal/update": {
|
"/core/settings/terminal/update": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -1991,8 +1991,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改系统配置 [key] => [value]",
|
"formatZH": "修改系统配置 [key] =\u003e [value]",
|
||||||
"formatEN": "update system setting [key] => [value]"
|
"formatEN": "update system setting [key] =\u003e [value]"
|
||||||
},
|
},
|
||||||
"/core/settings/upgrade": {
|
"/core/settings/upgrade": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -2000,8 +2000,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新系统 => [version]",
|
"formatZH": "更新系统 =\u003e [version]",
|
||||||
"formatEN": "upgrade system => [version]"
|
"formatEN": "upgrade system =\u003e [version]"
|
||||||
},
|
},
|
||||||
"/core/xapp/appQRCode": {
|
"/core/xapp/appQRCode": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -2459,282 +2459,6 @@
|
|||||||
"formatZH": "同步 SSL 证书 [primaryDomain]",
|
"formatZH": "同步 SSL 证书 [primaryDomain]",
|
||||||
"formatEN": "sync SSL certificate [primaryDomain]"
|
"formatEN": "sync SSL certificate [primaryDomain]"
|
||||||
},
|
},
|
||||||
"/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]"
|
|
||||||
},
|
|
||||||
"/cronjobs": {
|
"/cronjobs": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"type",
|
"type",
|
||||||
@@ -2888,8 +2612,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "首页应用 [key] => 显示:[value]",
|
"formatZH": "首页应用 [key] =\u003e 显示:[value]",
|
||||||
"formatEN": "app launcher [key] => show: [value]"
|
"formatEN": "app launcher [key] =\u003e show: [value]"
|
||||||
},
|
},
|
||||||
"/dashboard/quick/change": {
|
"/dashboard/quick/change": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -3017,7 +2741,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
|
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
|
||||||
"formatEN": "The description of the mysql database [name] is modified => [description]"
|
"formatEN": "The description of the mysql database [name] is modified =\u003e [description]"
|
||||||
},
|
},
|
||||||
"/databases/grants": {
|
"/databases/grants": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3098,7 +2822,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]",
|
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]",
|
||||||
"formatEN": "The description of the mongodb database [name] is modified => [description]"
|
"formatEN": "The description of the mongodb database [name] is modified =\u003e [description]"
|
||||||
},
|
},
|
||||||
"/databases/mongodb/password": {
|
"/databases/mongodb/password": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3183,7 +2907,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]",
|
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]",
|
||||||
"formatEN": "The description of the postgresql database [name] is modified => [description]"
|
"formatEN": "The description of the postgresql database [name] is modified =\u003e [description]"
|
||||||
},
|
},
|
||||||
"/databases/pg/password": {
|
"/databases/pg/password": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3288,8 +3012,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]",
|
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]",
|
||||||
"formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]"
|
"formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]"
|
||||||
},
|
},
|
||||||
"/databases/variables/update": {
|
"/databases/variables/update": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -3325,8 +3049,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]",
|
"formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]",
|
||||||
"formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]"
|
"formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]"
|
||||||
},
|
},
|
||||||
"/files/chunkdownload": {
|
"/files/chunkdownload": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3416,8 +3140,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改权限 [path] => [mode]",
|
"formatZH": "修改权限 [path] =\u003e [mode]",
|
||||||
"formatEN": "Change mode [path] => [mode]"
|
"formatEN": "Change mode [path] =\u003e [mode]"
|
||||||
},
|
},
|
||||||
"/files/move": {
|
"/files/move": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3426,8 +3150,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "移动文件 [oldPaths] => [newPath]",
|
"formatZH": "移动文件 [oldPaths] =\u003e [newPath]",
|
||||||
"formatEN": "Move [oldPaths] => [newPath]"
|
"formatEN": "Move [oldPaths] =\u003e [newPath]"
|
||||||
},
|
},
|
||||||
"/files/owner": {
|
"/files/owner": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3437,8 +3161,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改用户/组 [path] => [user]/[group]",
|
"formatZH": "修改用户/组 [path] =\u003e [user]/[group]",
|
||||||
"formatEN": "Change owner [path] => [user]/[group]"
|
"formatEN": "Change owner [path] =\u003e [user]/[group]"
|
||||||
},
|
},
|
||||||
"/files/preview": {
|
"/files/preview": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3472,8 +3196,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "重命名 [oldName] => [newName]",
|
"formatZH": "重命名 [oldName] =\u003e [newName]",
|
||||||
"formatEN": "Rename [oldName] => [newName]"
|
"formatEN": "Rename [oldName] =\u003e [newName]"
|
||||||
},
|
},
|
||||||
"/files/save": {
|
"/files/save": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3530,8 +3254,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "下载 url => [path]/[name]",
|
"formatZH": "下载 url =\u003e [path]/[name]",
|
||||||
"formatEN": "Download url => [path]/[name]"
|
"formatEN": "Download url =\u003e [path]/[name]"
|
||||||
},
|
},
|
||||||
"/files/wget/process/remove": {
|
"/files/wget/process/remove": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3673,13 +3397,11 @@
|
|||||||
"formatEN": "batch update Docker port guard policies"
|
"formatEN": "batch update Docker port guard policies"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/docker/policies/delete/batch": {
|
"/hosts/firewall/docker/policies/delete/batch": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"uuids"
|
|
||||||
],
|
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "删除 Docker 端口防护策略 [uuids]",
|
"formatZH": "删除 Docker 端口防护策略",
|
||||||
"formatEN": "delete Docker port guard policies [uuids]"
|
"formatEN": "delete Docker port guard policies"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/family/operate": {
|
"/hosts/firewall/family/operate": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3687,10 +3409,10 @@
|
|||||||
"family",
|
"family",
|
||||||
"operation"
|
"operation"
|
||||||
],
|
],
|
||||||
"formatEN": "[operation] [subsystem] [family] firewall chains",
|
|
||||||
"formatZH": "[operation] [subsystem] [family] 防火墙链",
|
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": []
|
"beforeFunctions": [],
|
||||||
|
"formatZH": "[operation] [subsystem] [family] 防火墙链",
|
||||||
|
"formatEN": "[operation] [subsystem] [family] firewall chains"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/filter/operate": {
|
"/hosts/firewall/filter/operate": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3739,13 +3461,11 @@
|
|||||||
"formatEN": "delete firewall rules"
|
"formatEN": "delete firewall rules"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/rules/reorder": {
|
"/hosts/firewall/rules/reorder": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"instanceKey"
|
|
||||||
],
|
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "调整防火墙规则顺序 [instanceKey]",
|
"formatZH": "调整防火墙规则顺序",
|
||||||
"formatEN": "reorder firewall rule [instanceKey]"
|
"formatEN": "reorder firewall rule"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/rules/reset": {
|
"/hosts/firewall/rules/reset": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -3755,22 +3475,20 @@
|
|||||||
"formatEN": "reset firewall rules"
|
"formatEN": "reset firewall rules"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/rules/update": {
|
"/hosts/firewall/rules/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"instanceKey"
|
|
||||||
],
|
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新防火墙规则 [instanceKey]",
|
"formatZH": "更新防火墙规则",
|
||||||
"formatEN": "update firewall rule [instanceKey]"
|
"formatEN": "update firewall rule"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/settings/ipv6": {
|
"/hosts/firewall/settings/ipv6": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"status"
|
"status"
|
||||||
],
|
],
|
||||||
"formatEN": "Set firewall IPv6 support to [status]",
|
|
||||||
"formatZH": "设置防火墙 IPv6 支持为 [status]",
|
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": []
|
"beforeFunctions": [],
|
||||||
|
"formatZH": "设置防火墙 IPv6 支持为 [status]",
|
||||||
|
"formatEN": "Set firewall IPv6 support to [status]"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/settings/operate": {
|
"/hosts/firewall/settings/operate": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3812,11 +3530,13 @@
|
|||||||
"formatEN": "update firewall port whitelist"
|
"formatEN": "update firewall port whitelist"
|
||||||
},
|
},
|
||||||
"/hosts/monitor/clean": {
|
"/hosts/monitor/clean": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "清空监控数据",
|
"formatZH": "清空 [type] 监控数据",
|
||||||
"formatEN": "clean monitor datas"
|
"formatEN": "clean [type] monitoring data"
|
||||||
},
|
},
|
||||||
"/hosts/monitor/setting/update": {
|
"/hosts/monitor/setting/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3888,8 +3608,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改 SSH 配置 [key] => [newValue]",
|
"formatZH": "修改 SSH 配置 [key] =\u003e [newValue]",
|
||||||
"formatEN": "update SSH setting [key] => [newValue]"
|
"formatEN": "update SSH setting [key] =\u003e [newValue]"
|
||||||
},
|
},
|
||||||
"/hosts/tool/config/set": {
|
"/hosts/tool/config/set": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4150,7 +3870,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "快照 [name] 描述信息修改 [description]",
|
"formatZH": "快照 [name] 描述信息修改 [description]",
|
||||||
"formatEN": "The description of the snapshot [name] is modified => [description]"
|
"formatEN": "The description of the snapshot [name] is modified =\u003e [description]"
|
||||||
},
|
},
|
||||||
"/settings/snapshot/import": {
|
"/settings/snapshot/import": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4242,8 +3962,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改系统配置 [key] => [value]",
|
"formatZH": "修改系统配置 [key] =\u003e [value]",
|
||||||
"formatEN": "update system setting [key] => [value]"
|
"formatEN": "update system setting [key] =\u003e [value]"
|
||||||
},
|
},
|
||||||
"/toolbox/clam": {
|
"/toolbox/clam": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4361,8 +4081,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改主机参数 [key] => [value]",
|
"formatZH": "修改主机参数 [key] =\u003e [value]",
|
||||||
"formatEN": "update device conf [key] => [value]"
|
"formatEN": "update device conf [key] =\u003e [value]"
|
||||||
},
|
},
|
||||||
"/toolbox/device/update/host": {
|
"/toolbox/device/update/host": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4371,8 +4091,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改主机 Host [key] => [value]",
|
"formatZH": "修改主机 Host [key] =\u003e [value]",
|
||||||
"formatEN": "update device host [key] => [value]"
|
"formatEN": "update device host [key] =\u003e [value]"
|
||||||
},
|
},
|
||||||
"/toolbox/device/update/swap": {
|
"/toolbox/device/update/swap": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4400,8 +4120,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改 Fail2ban 配置 [key] => [value]",
|
"formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]",
|
||||||
"formatEN": "update fail2ban conf [key] => [value]"
|
"formatEN": "update fail2ban conf [key] =\u003e [value]"
|
||||||
},
|
},
|
||||||
"/toolbox/ftp": {
|
"/toolbox/ftp": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4660,8 +4380,8 @@
|
|||||||
"output_value": "domain"
|
"output_value": "domain"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "修改默认 server => [domain]",
|
"formatZH": "修改默认 server =\u003e [domain]",
|
||||||
"formatEN": "Change default server => [domain]"
|
"formatEN": "Change default server =\u003e [domain]"
|
||||||
},
|
},
|
||||||
"/websites/del": {
|
"/websites/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -5424,8 +5144,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新防篡改信息 [website][path] => [status]",
|
"formatZH": "更新防篡改信息 [website][path] =\u003e [status]",
|
||||||
"formatEN": "update tamper info [website][path] => [status]"
|
"formatEN": "update tamper info [website][path] =\u003e [status]"
|
||||||
},
|
},
|
||||||
"/xpack/vllm/command-template/create": {
|
"/xpack/vllm/command-template/create": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -5504,6 +5224,282 @@
|
|||||||
"formatZH": "更新 vLLM [name]",
|
"formatZH": "更新 vLLM [name]",
|
||||||
"formatEN": "update 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": {
|
"/xpack/waf/block/export": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
|
|||||||
+14
-13
@@ -8,9 +8,9 @@ require (
|
|||||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||||
github.com/compose-spec/compose-go/v2 v2.15.0
|
github.com/compose-spec/compose-go/v2 v2.15.0
|
||||||
github.com/creack/pty v1.1.24
|
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/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/fsnotify/fsnotify v1.10.1
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
@@ -27,7 +27,7 @@ require (
|
|||||||
github.com/jinzhu/copier v0.4.0
|
github.com/jinzhu/copier v0.4.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/klauspost/compress v1.19.2
|
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/mholt/archiver/v4 v4.0.0-alpha.8
|
||||||
github.com/miekg/dns v1.1.73
|
github.com/miekg/dns v1.1.73
|
||||||
github.com/minio/minio-go/v7 v7.3.0
|
github.com/minio/minio-go/v7 v7.3.0
|
||||||
@@ -40,7 +40,7 @@ require (
|
|||||||
github.com/pkg/sftp v1.13.11
|
github.com/pkg/sftp v1.13.11
|
||||||
github.com/qiniu/go-sdk/v7 v7.27.0
|
github.com/qiniu/go-sdk/v7 v7.27.0
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
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/sirupsen/logrus v1.10.2
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
@@ -51,12 +51,12 @@ require (
|
|||||||
github.com/tklauser/go-sysconf v0.4.0
|
github.com/tklauser/go-sysconf v0.4.0
|
||||||
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
|
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
|
||||||
github.com/upyun/go-sdk v2.1.0+incompatible
|
github.com/upyun/go-sdk v2.1.0+incompatible
|
||||||
go.mongodb.org/mongo-driver/v2 v2.9.0
|
go.mongodb.org/mongo-driver/v2 v2.9.2
|
||||||
golang.org/x/crypto v0.56.0
|
golang.org/x/crypto v0.58.0
|
||||||
golang.org/x/net v0.58.0
|
golang.org/x/net v0.60.0
|
||||||
golang.org/x/sync v0.23.0
|
golang.org/x/sync v0.24.0
|
||||||
golang.org/x/sys v0.47.0
|
golang.org/x/sys v0.49.0
|
||||||
golang.org/x/text v0.41.0
|
golang.org/x/text v0.43.0
|
||||||
golang.org/x/time v0.16.0
|
golang.org/x/time v0.16.0
|
||||||
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
|
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
|
||||||
gopkg.in/ini.v1 v1.67.3
|
gopkg.in/ini.v1 v1.67.3
|
||||||
@@ -112,7 +112,7 @@ require (
|
|||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // 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/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||||
@@ -177,7 +177,7 @@ require (
|
|||||||
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.26 // 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/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.1 // indirect
|
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
@@ -203,7 +203,7 @@ require (
|
|||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
github.com/zeebo/xxh3 v1.1.0 // 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/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.45.0 // indirect
|
go.opentelemetry.io/otel v1.45.0 // indirect
|
||||||
@@ -216,6 +216,7 @@ require (
|
|||||||
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
|
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
|
||||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||||
golang.org/x/arch v0.26.0 // 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
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
google.golang.org/grpc v1.83.2 // indirect
|
google.golang.org/grpc v1.83.2 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
|||||||
@@ -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/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 h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
|
||||||
github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
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 h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
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 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
|
||||||
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
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 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
|
||||||
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
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 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
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=
|
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/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 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
||||||
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
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/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.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=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
@@ -567,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-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 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg=
|
||||||
github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
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 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
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=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
@@ -703,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/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 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-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 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.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||||
@@ -752,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/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 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
|
||||||
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
|
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/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/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=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
@@ -878,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.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 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||||
|
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 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.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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
@@ -954,6 +976,10 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
|||||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
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.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-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-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@@ -970,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-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-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.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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
@@ -997,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.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
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.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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -1053,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.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
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.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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -1079,6 +1110,8 @@ 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.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
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.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-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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -1156,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.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
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.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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -1171,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.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
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.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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -1189,6 +1228,10 @@ 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.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 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
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-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-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-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -1252,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.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 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
+11
-2
@@ -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.'
|
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.'
|
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.'
|
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'
|
ErrAppWarn: 'App status abnormal; check logs'
|
||||||
ErrAppParamKey: 'Invalid app parameter: {{ .name }}'
|
ErrAppParamKey: 'Invalid app parameter: {{ .name }}'
|
||||||
ErrAppUpgrade: 'App upgrade failed: {{ .name }} {{ .err }}'
|
ErrAppUpgrade: 'App upgrade failed: {{ .name }} {{ .err }}'
|
||||||
@@ -300,6 +303,9 @@ BuildCache: 'Build Cache'
|
|||||||
Volume: 'Volume'
|
Volume: 'Volume'
|
||||||
Network: "Network"
|
Network: "Network"
|
||||||
PruneStart: 'Cleaning in progress'
|
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'
|
ContainerBackupPrepare: 'Prepare container backup workspace'
|
||||||
ContainerBackupStop: 'Stop container before backup'
|
ContainerBackupStop: 'Stop container before backup'
|
||||||
ContainerBackupInspect: 'Backup container inspect and network'
|
ContainerBackupInspect: 'Backup container inspect and network'
|
||||||
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'Application type or name does not match'
|
|||||||
# alert
|
# alert
|
||||||
ErrAlertMethodNotSupported: 'This version does not support this alert method'
|
ErrAlertMethodNotSupported: 'This version does not support this alert method'
|
||||||
ErrAlertSync: 'Failed to sync alert message'
|
ErrAlertSync: 'Failed to sync alert message'
|
||||||
|
ErrWebhookTestFailed: 'Please check the Webhook configuration. See the system logs for details.'
|
||||||
|
|
||||||
# task - runtime
|
# task - runtime
|
||||||
ErrInstallExtension: 'Another extension install task is running'
|
ErrInstallExtension: 'Another extension install task is running'
|
||||||
|
|
||||||
# alert mail template
|
# alert mail template
|
||||||
PanelAlertTitle: "Panel Alert Notification"
|
PanelAlertTitle: "Panel Alert Notification"
|
||||||
TestAlertTitle: "Test Email - Verify Email Connectivity"
|
TestAlertTitle: "Panel Test Notification"
|
||||||
TestAlert: "This is a test email to verify that your email sending configuration is correct."
|
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."
|
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."
|
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."
|
ClamAlert: "Panel {{ .node }}{{ .ip }}: virus scan found {{ .num }} infected files. Log in to view details."
|
||||||
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 forwarding is disabled. Set net.ipv4.ip_f
|
|||||||
ErrFirewallRuleSavedApplyFailed: "The new rule configuration was saved, but applying it to the firewall failed. Retry by synchronizing: {{ .detail }}"
|
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."
|
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."
|
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"
|
ErrSSHPublicKey: "Enter a valid SSH public key"
|
||||||
ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase"
|
ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase"
|
||||||
|
|||||||
@@ -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.'
|
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.'
|
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.'
|
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'
|
ErrAppWarn: 'Estado anómalo, revise el log'
|
||||||
ErrAppParamKey: 'El campo de parámetro {{ .name }} es anómalo'
|
ErrAppParamKey: 'El campo de parámetro {{ .name }} es anómalo'
|
||||||
ErrAppUpgrade: 'La actualización de la aplicación {{ .name }} falló {{ .err }}'
|
ErrAppUpgrade: 'La actualización de la aplicación {{ .name }} falló {{ .err }}'
|
||||||
@@ -300,6 +303,9 @@ BuildCache: 'Caché de compilación'
|
|||||||
Volume: 'Volumen de almacenamiento'
|
Volume: 'Volumen de almacenamiento'
|
||||||
Network: 'Red'
|
Network: 'Red'
|
||||||
PruneStart: 'Limpiando en progreso espere...'
|
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'
|
ContainerBackupPrepare: 'Preparar espacio de respaldo del contenedor'
|
||||||
ContainerBackupStop: 'Detener contenedor antes del respaldo'
|
ContainerBackupStop: 'Detener contenedor antes del respaldo'
|
||||||
ContainerBackupInspect: 'Respaldar inspect y red del contenedor'
|
ContainerBackupInspect: 'Respaldar inspect y red del contenedor'
|
||||||
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'El tipo o nombre de la aplicación no coincide'
|
|||||||
# alerta
|
# alerta
|
||||||
ErrAlertMethodNotSupported: 'Esta versión no admite este método de 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'
|
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
|
# tarea - entorno de ejecución
|
||||||
ErrInstallExtension: 'Ya hay una tarea de instalación en curso, espere a que termine'
|
ErrInstallExtension: 'Ya hay una tarea de instalación en curso, espere a que termine'
|
||||||
|
|
||||||
# plantilla de correo de alerta
|
# plantilla de correo de alerta
|
||||||
PanelAlertTitle: 'Notificación de alerta del panel'
|
PanelAlertTitle: 'Notificación de alerta del panel'
|
||||||
TestAlertTitle: 'Correo de prueba - Verificar conectividad de correo'
|
TestAlertTitle: 'Notificación de prueba del panel'
|
||||||
TestAlert: 'Este es un correo de prueba para verificar que la configuración de envío de correos es correcta.'
|
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.'
|
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.'
|
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.'
|
ClamAlert: 'Su Panel {{ .node }}{{ .ip }}, la tarea de escaneo antivirus detectó {{ .num }} archivos infectados. Inicie sesión en el panel para ver los detalles.'
|
||||||
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "El reenvío IPv4 está desactivado. Configure
|
|||||||
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 }}"
|
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."
|
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."
|
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"
|
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"
|
ErrSSHPrivateKey: "Clave privada SSH no válida o contraseña incorrecta; las claves cifradas requieren la contraseña original"
|
||||||
|
|||||||
+11
-2
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'نام پروژه از پیکربندی Docker Comp
|
|||||||
ErrComposeNameInvalid: 'نام پروژه Compose نامعتبر است: باید با حرف کوچک یا عدد شروع شود، فقط شامل حروف کوچک، اعداد، خط تیره و زیرخط باشد و ۱ تا ۲۵۶ نویسه داشته باشد.'
|
ErrComposeNameInvalid: 'نام پروژه Compose نامعتبر است: باید با حرف کوچک یا عدد شروع شود، فقط شامل حروف کوچک، اعداد، خط تیره و زیرخط باشد و ۱ تا ۲۵۶ نویسه داشته باشد.'
|
||||||
ErrAppVersionUnavailable: 'این نسخه برنامه از سرویس راه دور حذف شده است. لطفاً نسخه دیگری را انتخاب کرده و دوباره تلاش کنید.'
|
ErrAppVersionUnavailable: 'این نسخه برنامه از سرویس راه دور حذف شده است. لطفاً نسخه دیگری را انتخاب کرده و دوباره تلاش کنید.'
|
||||||
ErrVllmGB10ProOnly: 'این نسخه vLLM فقط در نسخه حرفهای 1Panel در دسترس است.'
|
ErrVllmGB10ProOnly: 'این نسخه vLLM فقط در نسخه حرفهای 1Panel در دسترس است.'
|
||||||
|
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway فقط در همان نوع پایگاه داده (SQLite یا PostgreSQL) قابل ارتقا است.'
|
||||||
|
ErrAIGatewayRecoverDatabaseTypeMismatch: 'نسخه پشتیبان AI Gateway و برنامه مقصد باید از یک نوع پایگاه داده (SQLite یا PostgreSQL) استفاده کنند.'
|
||||||
|
ErrHermesUpgradeVersion: 'Hermes Agent فقط میتواند به نسخه جدیدتر و شناختهشدهای ارتقا یابد که تنظیمات ارتقا بین نسخههای اصلی اجازه میدهد. برای بازگشت به نسخه قدیمی، نسخه پشتیبان را بازیابی کنید.'
|
||||||
ErrAppWarn: 'وضعیت برنامه غیرعادی است؛ لاگها را بررسی کنید'
|
ErrAppWarn: 'وضعیت برنامه غیرعادی است؛ لاگها را بررسی کنید'
|
||||||
ErrAppParamKey: 'پارامتر برنامه نامعتبر است: {{ .name }}'
|
ErrAppParamKey: 'پارامتر برنامه نامعتبر است: {{ .name }}'
|
||||||
ErrAppUpgrade: 'ارتقاء برنامه ناموفق بود: {{ .name }} {{ .err }}'
|
ErrAppUpgrade: 'ارتقاء برنامه ناموفق بود: {{ .name }} {{ .err }}'
|
||||||
@@ -300,6 +303,9 @@ BuildCache: 'کش ساخت'
|
|||||||
Volume: 'حجم'
|
Volume: 'حجم'
|
||||||
Network: "شبکه"
|
Network: "شبکه"
|
||||||
PruneStart: 'پاکسازی در حال انجام است'
|
PruneStart: 'پاکسازی در حال انجام است'
|
||||||
|
NetworkCleanupConnected: "از شبکه [{{ .name }}] ({{ .id }}) صرفنظر شد: کانتینر متصل دارد یا در حال استفاده است؛ حذف نشد"
|
||||||
|
NetworkCleanupSummary: "پاکسازی شبکهها پایان یافت: حذفشده {{ .deleted }}، ردشده {{ .skipped }}، ناموفق {{ .failed }}"
|
||||||
|
NetworkCleanupPartialFailure: "پاکسازی برخی شبکهها ناموفق بود؛ گزارش وظیفه را بررسی کنید"
|
||||||
ContainerBackupPrepare: 'آمادهسازی فضای کاری پشتیبان کانتینر'
|
ContainerBackupPrepare: 'آمادهسازی فضای کاری پشتیبان کانتینر'
|
||||||
ContainerBackupStop: 'توقف کانتینر قبل از پشتیبانگیری'
|
ContainerBackupStop: 'توقف کانتینر قبل از پشتیبانگیری'
|
||||||
ContainerBackupInspect: 'بررسی و شبکه کانتینر پشتیبان'
|
ContainerBackupInspect: 'بررسی و شبکه کانتینر پشتیبان'
|
||||||
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'نوع یا نام برنامه مطابقت ندارد'
|
|||||||
# هشدار
|
# هشدار
|
||||||
ErrAlertMethodNotSupported: 'این نسخه از این روش هشدار پشتیبانی نمیکند'
|
ErrAlertMethodNotSupported: 'این نسخه از این روش هشدار پشتیبانی نمیکند'
|
||||||
ErrAlertSync: 'همگامسازی پیام هشدار ناموفق بود'
|
ErrAlertSync: 'همگامسازی پیام هشدار ناموفق بود'
|
||||||
|
ErrWebhookTestFailed: 'لطفاً پیکربندی وبهوک را بررسی کنید. برای جزئیات، گزارشهای سیستم را ببینید.'
|
||||||
|
|
||||||
# وظیفه - محیط اجرا
|
# وظیفه - محیط اجرا
|
||||||
ErrInstallExtension: 'وظیفه نصب پسوند دیگری در حال اجراست'
|
ErrInstallExtension: 'وظیفه نصب پسوند دیگری در حال اجراست'
|
||||||
|
|
||||||
# قالب ایمیل هشدار
|
# قالب ایمیل هشدار
|
||||||
PanelAlertTitle: "اعلان هشدار پنل"
|
PanelAlertTitle: "اعلان هشدار پنل"
|
||||||
TestAlertTitle: "ایمیل آزمایشی - تأیید اتصال ایمیل"
|
TestAlertTitle: "اعلان آزمایشی پنل"
|
||||||
TestAlert: "این یک ایمیل آزمایشی برای تأیید صحت پیکربندی ارسال ایمیل شما است."
|
TestAlertMessage: "این یک اعلان آزمایشی برای بررسی پیکربندی و اتصال کانال اعلان است."
|
||||||
LicenseExpirationAlert: "پنل {{ .node }}{{ .ip }}: مجوز در {{ .day }} روز منقضی میشود. برای مشاهده جزئیات وارد شوید."
|
LicenseExpirationAlert: "پنل {{ .node }}{{ .ip }}: مجوز در {{ .day }} روز منقضی میشود. برای مشاهده جزئیات وارد شوید."
|
||||||
CronJobFailedAlert: "پنل {{ .node }}{{ .ip }}: وظیفه زمانبندی {{ .name }} ناموفق بود. برای مشاهده جزئیات وارد شوید."
|
CronJobFailedAlert: "پنل {{ .node }}{{ .ip }}: وظیفه زمانبندی {{ .name }} ناموفق بود. برای مشاهده جزئیات وارد شوید."
|
||||||
ClamAlert: "پنل {{ .node }}{{ .ip }}: اسکن ویروس {{ .num }} فایل آلوده پیدا کرد. برای مشاهده جزئیات وارد شوید."
|
ClamAlert: "پنل {{ .node }}{{ .ip }}: اسکن ویروس {{ .num }} فایل آلوده پیدا کرد. برای مشاهده جزئیات وارد شوید."
|
||||||
@@ -719,6 +726,8 @@ ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش
|
|||||||
ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگامسازی دوباره تلاش کنید: {{ .detail }}"
|
ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگامسازی دوباره تلاش کنید: {{ .detail }}"
|
||||||
ErrFirewallRuleConflict: "یک قانون UFW با شرایط تطبیق یکسان و عملکرد متفاوت از قبل وجود دارد. قانون موجود را ویرایش کنید."
|
ErrFirewallRuleConflict: "یک قانون UFW با شرایط تطبیق یکسان و عملکرد متفاوت از قبل وجود دارد. قانون موجود را ویرایش کنید."
|
||||||
ErrForwardInterfaceNotFound: "رابط شبکه {{ .name }} مشخصشده در قانون انتقال وجود ندارد. نام رابط را بررسی کرده و دوباره تلاش کنید."
|
ErrForwardInterfaceNotFound: "رابط شبکه {{ .name }} مشخصشده در قانون انتقال وجود ندارد. نام رابط را بررسی کرده و دوباره تلاش کنید."
|
||||||
|
ErrIPv6RARisk: "فعالسازی هدایت IPv6 به دلیل خطر قطع شبکه مسدود شد. مستندات و گزارشهای سیستم را بررسی کنید."
|
||||||
|
ErrIPv6RACheckFailed: "فعالسازی هدایت IPv6 به دلیل عدم امکان بررسی پیکربندی شبکه مسدود شد. جزئیات در گزارشهای سیستم موجود است."
|
||||||
|
|
||||||
ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید"
|
ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید"
|
||||||
ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاریشده به عبارت عبور اصلی نیاز دارند"
|
ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاریشده به عبارت عبور اصلی نیاز دارند"
|
||||||
|
|||||||
+11
-2
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Docker Compose 設定またはファイルの親デ
|
|||||||
ErrComposeNameInvalid: 'Compose プロジェクト名の形式が正しくありません。小文字または数字で始まり、小文字、数字、ハイフン、アンダースコアのみを使用した 1~256 文字にしてください。'
|
ErrComposeNameInvalid: 'Compose プロジェクト名の形式が正しくありません。小文字または数字で始まり、小文字、数字、ハイフン、アンダースコアのみを使用した 1~256 文字にしてください。'
|
||||||
ErrAppVersionUnavailable: '現在のアプリケーションバージョンはリモートサービスから削除されています。別のバージョンを選択して再試行してください。'
|
ErrAppVersionUnavailable: '現在のアプリケーションバージョンはリモートサービスから削除されています。別のバージョンを選択して再試行してください。'
|
||||||
ErrVllmGB10ProOnly: 'この vLLM バージョンは 1Panel プロフェッショナル版でのみ利用できます。'
|
ErrVllmGB10ProOnly: 'この vLLM バージョンは 1Panel プロフェッショナル版でのみ利用できます。'
|
||||||
|
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway は同じデータベース種別(SQLite または PostgreSQL)間でのみアップグレードできます。'
|
||||||
|
ErrAIGatewayRecoverDatabaseTypeMismatch: 'AI Gateway のバックアップと復元先アプリのデータベース種別(SQLite または PostgreSQL)は一致している必要があります。'
|
||||||
|
ErrHermesUpgradeVersion: 'Hermes Agent は、認識可能でメジャーバージョン間のアップグレード設定に適合する新しいリリースにのみアップグレードできます。以前のバージョンに戻すには、バックアップから復元してください。'
|
||||||
ErrAppWarn: '異常な状態です。ログを確認してください'
|
ErrAppWarn: '異常な状態です。ログを確認してください'
|
||||||
ErrAppParamKey: 'パラメータ {{ .name }} フィールドが異常です'
|
ErrAppParamKey: 'パラメータ {{ .name }} フィールドが異常です'
|
||||||
ErrAppUpgrade: 'アプリケーション {{ .name }} のアップグレードに失敗しました {{ .err }}'
|
ErrAppUpgrade: 'アプリケーション {{ .name }} のアップグレードに失敗しました {{ .err }}'
|
||||||
@@ -300,6 +303,9 @@ BuildCache: 'ビルドキャッシュ'
|
|||||||
Volume: 'ストレージボリューム'
|
Volume: 'ストレージボリューム'
|
||||||
Network: 'ネットワーク'
|
Network: 'ネットワーク'
|
||||||
PruneStart: 'クリーンアップ中、しばらくお待ちください...'
|
PruneStart: 'クリーンアップ中、しばらくお待ちください...'
|
||||||
|
NetworkCleanupConnected: "ネットワーク [{{ .name }}] ({{ .id }}) をスキップしました:コンテナが接続されているか使用中のため、削除しませんでした"
|
||||||
|
NetworkCleanupSummary: "ネットワークのクリーンアップが完了しました:削除 {{ .deleted }} 件、スキップ {{ .skipped }} 件、失敗 {{ .failed }} 件"
|
||||||
|
NetworkCleanupPartialFailure: "一部のネットワークをクリーンアップできませんでした。タスクログを確認してください"
|
||||||
ContainerBackupPrepare: 'コンテナバックアップ作業領域を準備'
|
ContainerBackupPrepare: 'コンテナバックアップ作業領域を準備'
|
||||||
ContainerBackupStop: 'バックアップ前にコンテナを停止'
|
ContainerBackupStop: 'バックアップ前にコンテナを停止'
|
||||||
ContainerBackupInspect: 'コンテナ inspect とネットワーク情報をバックアップ'
|
ContainerBackupInspect: 'コンテナ inspect とネットワーク情報をバックアップ'
|
||||||
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'アプリケーションの種類または名前が一
|
|||||||
# アラート
|
# アラート
|
||||||
ErrAlertMethodNotSupported: 'このバージョンではこの通知方法は利用できません'
|
ErrAlertMethodNotSupported: 'このバージョンではこの通知方法は利用できません'
|
||||||
ErrAlertSync: 'アラーム情報の同期エラーです。確認してもう一度お試しください。'
|
ErrAlertSync: 'アラーム情報の同期エラーです。確認してもう一度お試しください。'
|
||||||
|
ErrWebhookTestFailed: 'Webhook の設定を確認してください。詳細はシステムログを参照してください。'
|
||||||
|
|
||||||
# タスク - ランタイム
|
# タスク - ランタイム
|
||||||
ErrInstallExtension: 'インストールタスクが進行中です、タスクが終了するのを待ってください'
|
ErrInstallExtension: 'インストールタスクが進行中です、タスクが終了するのを待ってください'
|
||||||
|
|
||||||
# アラートメールテンプレート
|
# アラートメールテンプレート
|
||||||
PanelAlertTitle: 'パネルアラート通知'
|
PanelAlertTitle: 'パネルアラート通知'
|
||||||
TestAlertTitle: 'テストメール - メール接続の確認'
|
TestAlertTitle: 'パネルのテスト通知'
|
||||||
TestAlert: 'これはテストメールです。メール送信設定が正しく構成されているかを確認します。'
|
TestAlertMessage: 'これは通知チャネルの設定と接続性を確認するためのテスト通知です。'
|
||||||
LicenseExpirationAlert: 'あなたの {{ .node }}{{ .ip }} パネル、ライセンスは {{ .day }} 日後に期限切れになります。詳細はパネルにログインして'
|
LicenseExpirationAlert: 'あなたの {{ .node }}{{ .ip }} パネル、ライセンスは {{ .day }} 日後に期限切れになります。詳細はパネルにログインして'
|
||||||
CronJobFailedAlert: 'あなたの {{ .node }}{{ .ip }} パネル、スケジュールタスク {{ .name }} の実行に失敗しました。詳細はパネルにログインして'
|
CronJobFailedAlert: 'あなたの {{ .node }}{{ .ip }} パネル、スケジュールタスク {{ .name }} の実行に失敗しました。詳細はパネルにログインして'
|
||||||
ClamAlert: 'あなたの {{ .node }}{{ .ip }} パネル、ウイルススキャンタスクで {{ .num }} 個の感染ファイルを検出しました。詳細はパネルにログインして'
|
ClamAlert: 'あなたの {{ .node }}{{ .ip }} パネル、ウイルススキャンタスクで {{ .num }} 個の感染ファイルを検出しました。詳細はパネルにログインして'
|
||||||
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファ
|
|||||||
ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}"
|
ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}"
|
||||||
ErrFirewallRuleConflict: "UFW に同じ一致条件でアクションが異なるルールが既に存在します。既存のルールを編集してください。"
|
ErrFirewallRuleConflict: "UFW に同じ一致条件でアクションが異なるルールが既に存在します。既存のルールを編集してください。"
|
||||||
ErrForwardInterfaceNotFound: "転送ルールで指定されたネットワークインターフェース {{ .name }} は存在しません。名前を確認して再試行してください。"
|
ErrForwardInterfaceNotFound: "転送ルールで指定されたネットワークインターフェース {{ .name }} は存在しません。名前を確認して再試行してください。"
|
||||||
|
ErrIPv6RARisk: "ネットワーク切断のおそれがあるため、IPv6 転送の有効化をブロックしました。ドキュメントとシステムログをご確認ください。"
|
||||||
|
ErrIPv6RACheckFailed: "ネットワーク設定を確認できないため、IPv6 転送の有効化をブロックしました。詳細はシステムログをご確認ください。"
|
||||||
|
|
||||||
ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください"
|
ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください"
|
||||||
ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です"
|
ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です"
|
||||||
|
|||||||
+11
-2
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Docker Compose 구성 또는 파일의 상위 디
|
|||||||
ErrComposeNameInvalid: 'Compose 프로젝트 이름 형식이 잘못되었습니다. 소문자 또는 숫자로 시작하고 소문자, 숫자, 하이픈, 밑줄만 사용하여 1~256자로 입력하세요.'
|
ErrComposeNameInvalid: 'Compose 프로젝트 이름 형식이 잘못되었습니다. 소문자 또는 숫자로 시작하고 소문자, 숫자, 하이픈, 밑줄만 사용하여 1~256자로 입력하세요.'
|
||||||
ErrAppVersionUnavailable: '현재 애플리케이션 버전이 원격 서비스에서 제거되었습니다. 다른 버전을 선택한 후 다시 시도해 주세요.'
|
ErrAppVersionUnavailable: '현재 애플리케이션 버전이 원격 서비스에서 제거되었습니다. 다른 버전을 선택한 후 다시 시도해 주세요.'
|
||||||
ErrVllmGB10ProOnly: '이 vLLM 버전은 1Panel 프로페셔널 에디션에서만 사용할 수 있습니다.'
|
ErrVllmGB10ProOnly: '이 vLLM 버전은 1Panel 프로페셔널 에디션에서만 사용할 수 있습니다.'
|
||||||
|
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway는 동일한 데이터베이스 유형(SQLite 또는 PostgreSQL) 내에서만 업그레이드할 수 있습니다.'
|
||||||
|
ErrAIGatewayRecoverDatabaseTypeMismatch: 'AI Gateway 백업과 대상 애플리케이션의 데이터베이스 유형(SQLite 또는 PostgreSQL)이 같아야 합니다.'
|
||||||
|
ErrHermesUpgradeVersion: 'Hermes Agent는 인식 가능하고 주 버전 간 업그레이드 설정을 충족하는 최신 릴리스로만 업그레이드할 수 있습니다. 이전 버전으로 되돌리려면 백업을 복원하세요.'
|
||||||
ErrAppWarn: '비정상적인 상태입니다. 로그를 확인해 주세요.'
|
ErrAppWarn: '비정상적인 상태입니다. 로그를 확인해 주세요.'
|
||||||
ErrAppParamKey: '매개변수 {{ .name }} 필드가 비정상입니다.'
|
ErrAppParamKey: '매개변수 {{ .name }} 필드가 비정상입니다.'
|
||||||
ErrAppUpgrade: '애플리케이션 {{ .name }} 업그레이드에 실패했습니다 {{ .err }}'
|
ErrAppUpgrade: '애플리케이션 {{ .name }} 업그레이드에 실패했습니다 {{ .err }}'
|
||||||
@@ -300,6 +303,9 @@ BuildCache: '빌드 캐시'
|
|||||||
Volume: '스토리지 볼륨'
|
Volume: '스토리지 볼륨'
|
||||||
Network: '네트워크'
|
Network: '네트워크'
|
||||||
PruneStart: '정리 중입니다. 잠시만 기다려주세요...'
|
PruneStart: '정리 중입니다. 잠시만 기다려주세요...'
|
||||||
|
NetworkCleanupConnected: "네트워크 [{{ .name }}] ({{ .id }}) 건너뜀: 연결된 컨테이너가 있거나 사용 중이므로 삭제하지 않음"
|
||||||
|
NetworkCleanupSummary: "네트워크 정리 완료: 삭제 {{ .deleted }}개, 건너뜀 {{ .skipped }}개, 실패 {{ .failed }}개"
|
||||||
|
NetworkCleanupPartialFailure: "일부 네트워크를 정리하지 못했습니다. 작업 로그를 확인하세요"
|
||||||
ContainerBackupPrepare: '컨테이너 백업 작업 공간 준비'
|
ContainerBackupPrepare: '컨테이너 백업 작업 공간 준비'
|
||||||
ContainerBackupStop: '백업 전에 컨테이너 중지'
|
ContainerBackupStop: '백업 전에 컨테이너 중지'
|
||||||
ContainerBackupInspect: '컨테이너 inspect 및 네트워크 정보 백업'
|
ContainerBackupInspect: '컨테이너 inspect 및 네트워크 정보 백업'
|
||||||
@@ -628,14 +634,15 @@ AppAttributesNotMatch: '애플리케이션 유형 또는 이름이 일치하지
|
|||||||
# 알림
|
# 알림
|
||||||
ErrAlertMethodNotSupported: '현재 버전에서는 이 알림 방식을 지원하지 않습니다'
|
ErrAlertMethodNotSupported: '현재 버전에서는 이 알림 방식을 지원하지 않습니다'
|
||||||
ErrAlertSync: '알람 정보 동기화 오류입니다. 확인하고 다시 시도하세요'
|
ErrAlertSync: '알람 정보 동기화 오류입니다. 확인하고 다시 시도하세요'
|
||||||
|
ErrWebhookTestFailed: 'Webhook 설정을 확인하세요. 자세한 원인은 시스템 로그를 확인하세요.'
|
||||||
|
|
||||||
# 작업 - 런타임
|
# 작업 - 런타임
|
||||||
ErrInstallExtension: '이미 설치 작업이 진행 중입니다. 작업이 완료될 때까지 기다려 주세요.'
|
ErrInstallExtension: '이미 설치 작업이 진행 중입니다. 작업이 완료될 때까지 기다려 주세요.'
|
||||||
|
|
||||||
# 알림 메일 템플릿
|
# 알림 메일 템플릿
|
||||||
PanelAlertTitle: '패널 알림 통지'
|
PanelAlertTitle: '패널 알림 통지'
|
||||||
TestAlertTitle: '테스트 이메일 - 이메일 연결 확인'
|
TestAlertTitle: '패널 테스트 알림'
|
||||||
TestAlert: '이것은 테스트 이메일이며 이메일 발송 설정이 올바른지 확인합니다.'
|
TestAlertMessage: '알림 채널 설정 및 연결 상태를 확인하기 위한 테스트 알림입니다.'
|
||||||
LicenseExpirationAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 라이센스가 {{ .day }}일 후에 만료됩니다. 자세한 내용은 패널에 로그인하십시오.'
|
LicenseExpirationAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 라이센스가 {{ .day }}일 후에 만료됩니다. 자세한 내용은 패널에 로그인하십시오.'
|
||||||
CronJobFailedAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 예약 작업 {{ .name }} 실행에 실패했습니다. 자세한 내용은 패널에 로그인하십시오.'
|
CronJobFailedAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 예약 작업 {{ .name }} 실행에 실패했습니다. 자세한 내용은 패널에 로그인하십시오.'
|
||||||
ClamAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 바이러스 스캔 작업에서 {{ .num }}개의 감염 파일이 감지되었습니다. 자세한 내용은 패널에 로그인하십시오.'
|
ClamAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 바이러스 스캔 작업에서 {{ .num }}개의 감염 파일이 감지되었습니다. 자세한 내용은 패널에 로그인하십시오.'
|
||||||
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다
|
|||||||
ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}"
|
ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}"
|
||||||
ErrFirewallRuleConflict: "UFW에 일치 조건이 같지만 동작이 다른 규칙이 이미 있습니다. 기존 규칙을 편집하세요."
|
ErrFirewallRuleConflict: "UFW에 일치 조건이 같지만 동작이 다른 규칙이 이미 있습니다. 기존 규칙을 편집하세요."
|
||||||
ErrForwardInterfaceNotFound: "전달 규칙에 지정된 네트워크 인터페이스 {{ .name }}이 존재하지 않습니다. 이름을 확인한 후 다시 시도하세요."
|
ErrForwardInterfaceNotFound: "전달 규칙에 지정된 네트워크 인터페이스 {{ .name }}이 존재하지 않습니다. 이름을 확인한 후 다시 시도하세요."
|
||||||
|
ErrIPv6RARisk: "네트워크 연결이 끊길 위험이 있어 IPv6 전달 활성화를 차단했습니다. 문서와 시스템 로그를 확인하세요."
|
||||||
|
ErrIPv6RACheckFailed: "네트워크 구성을 확인할 수 없어 IPv6 전달 활성화를 차단했습니다. 자세한 내용은 시스템 로그를 확인하세요."
|
||||||
|
|
||||||
ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요"
|
ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요"
|
||||||
ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다"
|
ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다"
|
||||||
|
|||||||
+11
-2
@@ -130,6 +130,9 @@ ErrComposeProjectNameEmpty: 'ບໍ່ສາມາດກຳນົດຊື່ໂ
|
|||||||
ErrComposeNameInvalid: 'ຊື່ໂຄງການ Compose ບໍ່ຖືກຕ້ອງ: ຕ້ອງເລີ່ມດ້ວຍຕົວພິມນ້ອຍ ຫຼື ຕົວເລກ, ໃຊ້ໄດ້ສະເພາະຕົວພິມນ້ອຍ, ຕົວເລກ, ຂີດກາງ ແລະ ຂີດລຸ່ມ, ຄວາມຍາວ 1-256 ຕົວອັກສອນ.'
|
ErrComposeNameInvalid: 'ຊື່ໂຄງການ Compose ບໍ່ຖືກຕ້ອງ: ຕ້ອງເລີ່ມດ້ວຍຕົວພິມນ້ອຍ ຫຼື ຕົວເລກ, ໃຊ້ໄດ້ສະເພາະຕົວພິມນ້ອຍ, ຕົວເລກ, ຂີດກາງ ແລະ ຂີດລຸ່ມ, ຄວາມຍາວ 1-256 ຕົວອັກສອນ.'
|
||||||
ErrAppVersionUnavailable: 'ເວີຊັນແອັບນີ້ຖືກລຶບອອກຈາກບໍລິການທາງໄກແລ້ວ ກະລຸນາເລືອກເວີຊັນອື່ນແລ້ວລອງໃໝ່'
|
ErrAppVersionUnavailable: 'ເວີຊັນແອັບນີ້ຖືກລຶບອອກຈາກບໍລິການທາງໄກແລ້ວ ກະລຸນາເລືອກເວີຊັນອື່ນແລ້ວລອງໃໝ່'
|
||||||
ErrVllmGB10ProOnly: 'ເວີຊັນ vLLM ນີ້ມີໃຫ້ໃຊ້ສະເພາະໃນ 1Panel Professional Edition ເທົ່ານັ້ນ.'
|
ErrVllmGB10ProOnly: 'ເວີຊັນ vLLM ນີ້ມີໃຫ້ໃຊ້ສະເພາະໃນ 1Panel Professional Edition ເທົ່ານັ້ນ.'
|
||||||
|
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway ສາມາດອັບເກຣດໄດ້ພາຍໃນປະເພດຖານຂໍ້ມູນດຽວກັນເທົ່ານັ້ນ (SQLite ຫຼື PostgreSQL).'
|
||||||
|
ErrAIGatewayRecoverDatabaseTypeMismatch: 'ຂໍ້ມູນສຳຮອງ AI Gateway ແລະ ແອັບເປົ້າໝາຍຕ້ອງໃຊ້ປະເພດຖານຂໍ້ມູນດຽວກັນ (SQLite ຫຼື PostgreSQL).'
|
||||||
|
ErrHermesUpgradeVersion: 'Hermes Agent ສາມາດອັບເກຣດໄປຫາລຸ້ນໃໝ່ກວ່າທີ່ຮູ້ຈັກ ແລະ ອະນຸຍາດໂດຍການຕັ້ງຄ່າອັບເກຣດຂ້າມລຸ້ນຫຼັກເທົ່ານັ້ນ. ຖ້າຕ້ອງການກັບໄປລຸ້ນເກົ່າ ໃຫ້ກູ້ຄືນຈາກຂໍ້ມູນສຳຮອງ.'
|
||||||
ErrAppWarn: 'ສະຖານະແອັບຜິດປົກກະຕິ; ກະລຸນາກວດສອບບັນທຶກ'
|
ErrAppWarn: 'ສະຖານະແອັບຜິດປົກກະຕິ; ກະລຸນາກວດສອບບັນທຶກ'
|
||||||
ErrAppParamKey: 'ພາຣາມິເຕີຂອງແອັບບໍ່ຖືກຕ້ອງ: {{ .name }}'
|
ErrAppParamKey: 'ພາຣາມິເຕີຂອງແອັບບໍ່ຖືກຕ້ອງ: {{ .name }}'
|
||||||
ErrAppUpgrade: 'ອັບເກຣດແອັບລົ້ມເຫຼວ: {{ .name }} {{ .err }}'
|
ErrAppUpgrade: 'ອັບເກຣດແອັບລົ້ມເຫຼວ: {{ .name }} {{ .err }}'
|
||||||
@@ -290,6 +293,9 @@ BuildCache: 'ແຄດການສ້າງ'
|
|||||||
Volume: 'ວໍລຸມ (Volume)'
|
Volume: 'ວໍລຸມ (Volume)'
|
||||||
Network: "ເຄືອຂ່າຍ"
|
Network: "ເຄືອຂ່າຍ"
|
||||||
PruneStart: 'ກຳລັງດຳເນີນການທຳຄວາມສະອາດ'
|
PruneStart: 'ກຳລັງດຳເນີນການທຳຄວາມສະອາດ'
|
||||||
|
NetworkCleanupConnected: "ຂ້າມເຄືອຂ່າຍ [{{ .name }}] ({{ .id }}): ມີຄອນເທນເນີເຊື່ອມຕໍ່ຢູ່ ຫຼື ເຄືອຂ່າຍກຳລັງຖືກໃຊ້ງານ; ບໍ່ໄດ້ລຶບ"
|
||||||
|
NetworkCleanupSummary: "ການລ້າງເຄືອຂ່າຍສຳເລັດ: ລຶບແລ້ວ {{ .deleted }}, ຂ້າມ {{ .skipped }}, ລົ້ມເຫຼວ {{ .failed }}"
|
||||||
|
NetworkCleanupPartialFailure: "ບໍ່ສາມາດລ້າງບາງເຄືອຂ່າຍໄດ້; ກະລຸນາເບິ່ງບັນທຶກວຽກ"
|
||||||
ContainerBackupPrepare: 'ກຽມພື້ນທີ່ເຮັດວຽກສຳລັບສຳຮອງຂໍ້ມູນຄອນເທນເນີ'
|
ContainerBackupPrepare: 'ກຽມພື້ນທີ່ເຮັດວຽກສຳລັບສຳຮອງຂໍ້ມູນຄອນເທນເນີ'
|
||||||
ContainerBackupStop: 'ຢຸດຄອນເທນເນີກ່ອນການສຳຮອງຂໍ້ມູນ'
|
ContainerBackupStop: 'ຢຸດຄອນເທນເນີກ່ອນການສຳຮອງຂໍ້ມູນ'
|
||||||
ContainerBackupInspect: 'ກວດສອບຄອນເທນເນີ ແລະ ເຄືອຂ່າຍສຳລັບການສຳຮອງຂໍ້ມູນ'
|
ContainerBackupInspect: 'ກວດສອບຄອນເທນເນີ ແລະ ເຄືອຂ່າຍສຳລັບການສຳຮອງຂໍ້ມູນ'
|
||||||
@@ -618,14 +624,15 @@ AppAttributesNotMatch: 'ປະເພດ ຫຼື ຊື່ຂອງແອັບ
|
|||||||
|
|
||||||
#alert
|
#alert
|
||||||
ErrAlertSync: 'ຊິງຄ໌ຂໍ້ຄວາມແຈ້ງເຕືອນລົ້ມເຫຼວ'
|
ErrAlertSync: 'ຊິງຄ໌ຂໍ້ຄວາມແຈ້ງເຕືອນລົ້ມເຫຼວ'
|
||||||
|
ErrWebhookTestFailed: 'ກະລຸນາກວດສອບການຕັ້ງຄ່າ Webhook. ເບິ່ງບັນທຶກລະບົບສຳລັບລາຍລະອຽດ.'
|
||||||
|
|
||||||
#task - runtime
|
#task - runtime
|
||||||
ErrInstallExtension: 'ມີວຽກຕິດຕັ້ງສ່ວນເສີມອື່ນກຳລັງເຮັດວຽກຢູ່'
|
ErrInstallExtension: 'ມີວຽກຕິດຕັ້ງສ່ວນເສີມອື່ນກຳລັງເຮັດວຽກຢູ່'
|
||||||
|
|
||||||
# alert mail template
|
# alert mail template
|
||||||
PanelAlertTitle: "ການແຈ້ງເຕືອນຈາກແຜງຄວບຄຸມ"
|
PanelAlertTitle: "ການແຈ້ງເຕືອນຈາກແຜງຄວບຄຸມ"
|
||||||
TestAlertTitle: "ອີເມວທົດສອບ - ກວດສອບການເຊື່ອມຕໍ່ອີເມວ"
|
TestAlertTitle: "ການແຈ້ງເຕືອນທົດສອບຈາກແຜງຄວບຄຸມ"
|
||||||
TestAlert: "ນີ້ແມ່ນອີເມວທົດສອບເພື່ອຢືນຢັນວ່າການຕັ້ງຄ່າການສົ່ງອີເມວຂອງທ່ານຖືກຕ້ອງ."
|
TestAlertMessage: "ນີ້ແມ່ນການແຈ້ງເຕືອນທົດສອບເພື່ອກວດສອບການຕັ້ງຄ່າ ແລະ ການເຊື່ອມຕໍ່ຂອງຊ່ອງທາງແຈ້ງເຕືອນ."
|
||||||
LicenseExpirationAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໃບອະນຸຍາດຈະໝົດອາຍຸໃນ {{ .day }} ວັນ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
|
LicenseExpirationAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໃບອະນຸຍາດຈະໝົດອາຍຸໃນ {{ .day }} ວັນ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
|
||||||
CronJobFailedAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ວຽກທີ່ຕັ້ງເວລາ {{ .name }} ລົ້ມເຫຼວ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
|
CronJobFailedAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ວຽກທີ່ຕັ້ງເວລາ {{ .name }} ລົ້ມເຫຼວ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
|
||||||
ClamAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ການສະແກນໄວຣັສພົບ {{ .num }} ໄຟລ໌ທີ່ຕິດເຊື້ອ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
|
ClamAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ການສະແກນໄວຣັສພົບ {{ .num }} ໄຟລ໌ທີ່ຕິດເຊື້ອ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
|
||||||
@@ -709,6 +716,8 @@ ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກ
|
|||||||
ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}"
|
ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}"
|
||||||
ErrFirewallRuleConflict: "ມີກົດ UFW ທີ່ມີເງື່ອນໄຂກົງກັນແຕ່ການດຳເນີນການຕ່າງກັນແລ້ວ. ກະລຸນາແກ້ໄຂກົດທີ່ມີຢູ່."
|
ErrFirewallRuleConflict: "ມີກົດ UFW ທີ່ມີເງື່ອນໄຂກົງກັນແຕ່ການດຳເນີນການຕ່າງກັນແລ້ວ. ກະລຸນາແກ້ໄຂກົດທີ່ມີຢູ່."
|
||||||
ErrForwardInterfaceNotFound: "ບໍ່ພົບອິນເຕີເຟດເຄືອຂ່າຍ {{ .name }} ທີ່ລະບຸໃນກົດສົ່ງຕໍ່. ກະລຸນາກວດສອບຊື່ ແລ້ວລອງໃໝ່."
|
ErrForwardInterfaceNotFound: "ບໍ່ພົບອິນເຕີເຟດເຄືອຂ່າຍ {{ .name }} ທີ່ລະບຸໃນກົດສົ່ງຕໍ່. ກະລຸນາກວດສອບຊື່ ແລ້ວລອງໃໝ່."
|
||||||
|
ErrIPv6RARisk: "ການສົ່ງຕໍ່ IPv6 ຖືກບລັອກເພື່ອປ້ອງກັນຄວາມສ່ຽງທີ່ເຄືອຂ່າຍຈະຂາດການເຊື່ອມຕໍ່. ເບິ່ງເອກະສານ ແລະ ບັນທຶກລະບົບ."
|
||||||
|
ErrIPv6RACheckFailed: "ການສົ່ງຕໍ່ IPv6 ຖືກບລັອກເນື່ອງຈາກບໍ່ສາມາດກວດສອບການຕັ້ງຄ່າເຄືອຂ່າຍໄດ້. ເບິ່ງລາຍລະອຽດໃນບັນທຶກລະບົບ."
|
||||||
|
|
||||||
ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ"
|
ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ"
|
||||||
ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ"
|
ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ"
|
||||||
|
|||||||
+11
-2
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Nama projek tidak dapat diperoleh daripada konfigur
|
|||||||
ErrComposeNameInvalid: 'Nama projek Compose tidak sah: mesti bermula dengan huruf kecil atau nombor, hanya mengandungi huruf kecil, nombor, tanda sempang dan garis bawah, serta sepanjang 1-256 aksara.'
|
ErrComposeNameInvalid: 'Nama projek Compose tidak sah: mesti bermula dengan huruf kecil atau nombor, hanya mengandungi huruf kecil, nombor, tanda sempang dan garis bawah, serta sepanjang 1-256 aksara.'
|
||||||
ErrAppVersionUnavailable: 'Versi aplikasi ini telah dialih keluar daripada perkhidmatan jauh. Sila pilih versi lain dan cuba lagi.'
|
ErrAppVersionUnavailable: 'Versi aplikasi ini telah dialih keluar daripada perkhidmatan jauh. Sila pilih versi lain dan cuba lagi.'
|
||||||
ErrVllmGB10ProOnly: 'Versi vLLM ini hanya tersedia dalam 1Panel Edisi Profesional.'
|
ErrVllmGB10ProOnly: 'Versi vLLM ini hanya tersedia dalam 1Panel Edisi Profesional.'
|
||||||
|
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway hanya boleh dinaik taraf dalam jenis pangkalan data yang sama (SQLite atau PostgreSQL).'
|
||||||
|
ErrAIGatewayRecoverDatabaseTypeMismatch: 'Sandaran AI Gateway dan aplikasi sasaran mesti menggunakan jenis pangkalan data yang sama (SQLite atau PostgreSQL).'
|
||||||
|
ErrHermesUpgradeVersion: 'Hermes Agent hanya boleh dinaik taraf kepada keluaran lebih baharu yang dikenal pasti dan dibenarkan oleh tetapan naik taraf antara versi utama. Untuk kembali kepada versi lama, pulihkan sandaran.'
|
||||||
ErrAppWarn: 'Status tidak normal, sila semak log'
|
ErrAppWarn: 'Status tidak normal, sila semak log'
|
||||||
ErrAppParamKey: 'Parameter {{ .name }} medan tidak normal'
|
ErrAppParamKey: 'Parameter {{ .name }} medan tidak normal'
|
||||||
ErrAppUpgrade: 'Peningkatan {{ .name }} aplikasi gagal {{ .err }}'
|
ErrAppUpgrade: 'Peningkatan {{ .name }} aplikasi gagal {{ .err }}'
|
||||||
@@ -300,6 +303,9 @@ BuildCache: 'Cache binaan'
|
|||||||
Volume: 'Jilid storan'
|
Volume: 'Jilid storan'
|
||||||
Network: 'Rangkaian'
|
Network: 'Rangkaian'
|
||||||
PruneStart: 'Pembersihan sedang dijalankan, sila tunggu...'
|
PruneStart: 'Pembersihan sedang dijalankan, sila tunggu...'
|
||||||
|
NetworkCleanupConnected: "Rangkaian [{{ .name }}] ({{ .id }}) dilangkau: terdapat kontena yang disambungkan atau rangkaian sedang digunakan; tidak dipadamkan"
|
||||||
|
NetworkCleanupSummary: "Pembersihan rangkaian selesai: dipadamkan {{ .deleted }}, dilangkau {{ .skipped }}, gagal {{ .failed }}"
|
||||||
|
NetworkCleanupPartialFailure: "Sesetengah rangkaian tidak dapat dibersihkan; semak log tugas"
|
||||||
ContainerBackupPrepare: 'Sediakan ruang kerja sandaran kontena'
|
ContainerBackupPrepare: 'Sediakan ruang kerja sandaran kontena'
|
||||||
ContainerBackupStop: 'Hentikan kontena sebelum sandaran'
|
ContainerBackupStop: 'Hentikan kontena sebelum sandaran'
|
||||||
ContainerBackupInspect: 'Sandarkan inspect dan rangkaian kontena'
|
ContainerBackupInspect: 'Sandarkan inspect dan rangkaian kontena'
|
||||||
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'Jenis atau nama aplikasi tidak sepadan'
|
|||||||
# amaran
|
# amaran
|
||||||
ErrAlertMethodNotSupported: 'Versi ini tidak menyokong kaedah amaran ini'
|
ErrAlertMethodNotSupported: 'Versi ini tidak menyokong kaedah amaran ini'
|
||||||
ErrAlertSync: 'Ralat penyegerakan maklumat penggera, sila semak dan cuba lagi'
|
ErrAlertSync: 'Ralat penyegerakan maklumat penggera, sila semak dan cuba lagi'
|
||||||
|
ErrWebhookTestFailed: 'Sila semak konfigurasi Webhook. Lihat log sistem untuk butiran lanjut.'
|
||||||
|
|
||||||
# tugas - runtime
|
# tugas - runtime
|
||||||
ErrInstallExtension: 'Tugas pemasangan sudah sedang berjalan, silakan tunggu tugas selesai'
|
ErrInstallExtension: 'Tugas pemasangan sudah sedang berjalan, silakan tunggu tugas selesai'
|
||||||
|
|
||||||
# templat mel amaran
|
# templat mel amaran
|
||||||
PanelAlertTitle: 'Notifikasi Amaran Panel'
|
PanelAlertTitle: 'Notifikasi Amaran Panel'
|
||||||
TestAlertTitle: 'E-mel Ujian - Sahkan Sambungan E-mel'
|
TestAlertTitle: 'Notifikasi Ujian Panel'
|
||||||
TestAlert: 'Ini adalah e-mel ujian untuk mengesahkan konfigurasi penghantaran e-mel anda betul.'
|
TestAlertMessage: 'Ini ialah notifikasi ujian untuk mengesahkan konfigurasi dan kesambungan saluran notifikasi.'
|
||||||
LicenseExpirationAlert: 'Panel {{ .node }}{{ .ip }} Anda, lesen akan tamat tempoh dalam {{ .day }} hari. Sila log masuk ke panel untuk butiran lanjut.'
|
LicenseExpirationAlert: 'Panel {{ .node }}{{ .ip }} Anda, lesen akan tamat tempoh dalam {{ .day }} hari. Sila log masuk ke panel untuk butiran lanjut.'
|
||||||
CronJobFailedAlert: 'Panel {{ .node }}{{ .ip }} Anda, tugas jadual {{ .name }} gagal dilaksanakan. Sila log masuk ke panel untuk butiran lanjut.'
|
CronJobFailedAlert: 'Panel {{ .node }}{{ .ip }} Anda, tugas jadual {{ .name }} gagal dilaksanakan. Sila log masuk ke panel untuk butiran lanjut.'
|
||||||
ClamAlert: 'Panel {{ .node }}{{ .ip }} Anda, tugas imbasan virus mengesan {{ .num }} fail yang dijangkiti. Sila log masuk ke panel untuk butiran lanjut.'
|
ClamAlert: 'Panel {{ .node }}{{ .ip }} Anda, tugas imbasan virus mengesan {{ .num }} fail yang dijangkiti. Sila log masuk ke panel untuk butiran lanjut.'
|
||||||
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "Pemajuan IPv4 dilumpuhkan. Tetapkan net.ipv4.i
|
|||||||
ErrFirewallRuleSavedApplyFailed: "Konfigurasi peraturan baharu telah disimpan, tetapi gagal digunakan pada tembok api. Cuba lagi melalui penyegerakan: {{ .detail }}"
|
ErrFirewallRuleSavedApplyFailed: "Konfigurasi peraturan baharu telah disimpan, tetapi gagal digunakan pada tembok api. Cuba lagi melalui penyegerakan: {{ .detail }}"
|
||||||
ErrFirewallRuleConflict: "Peraturan UFW dengan syarat padanan yang sama tetapi tindakan berbeza sudah wujud. Sila edit peraturan sedia ada."
|
ErrFirewallRuleConflict: "Peraturan UFW dengan syarat padanan yang sama tetapi tindakan berbeza sudah wujud. Sila edit peraturan sedia ada."
|
||||||
ErrForwardInterfaceNotFound: "Antara muka rangkaian {{ .name }} yang dinyatakan dalam peraturan pemajuan tidak wujud. Semak namanya dan cuba lagi."
|
ErrForwardInterfaceNotFound: "Antara muka rangkaian {{ .name }} yang dinyatakan dalam peraturan pemajuan tidak wujud. Semak namanya dan cuba lagi."
|
||||||
|
ErrIPv6RARisk: "Pemajuan IPv6 disekat bagi mengelakkan risiko kehilangan sambungan rangkaian. Rujuk dokumentasi dan log sistem."
|
||||||
|
ErrIPv6RACheckFailed: "Pemajuan IPv6 disekat kerana konfigurasi rangkaian tidak dapat diperiksa. Lihat log sistem untuk butiran."
|
||||||
|
|
||||||
ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah"
|
ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah"
|
||||||
ErrSSHPrivateKey: "Kunci peribadi SSH tidak sah atau frasa laluan salah; kunci yang disulitkan memerlukan frasa laluan asal"
|
ErrSSHPrivateKey: "Kunci peribadi SSH tidak sah atau frasa laluan salah; kunci yang disulitkan memerlukan frasa laluan asal"
|
||||||
|
|||||||
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Não foi possível derivar um nome de projeto da co
|
|||||||
ErrComposeNameInvalid: 'Nome de projeto Compose inválido: deve começar com uma letra minúscula ou número, conter apenas letras minúsculas, números, hífens e sublinhados e ter de 1 a 256 caracteres.'
|
ErrComposeNameInvalid: 'Nome de projeto Compose inválido: deve começar com uma letra minúscula ou número, conter apenas letras minúsculas, números, hífens e sublinhados e ter de 1 a 256 caracteres.'
|
||||||
ErrAppVersionUnavailable: 'Esta versão do aplicativo foi removida do serviço remoto. Selecione outra versão e tente novamente.'
|
ErrAppVersionUnavailable: 'Esta versão do aplicativo foi removida do serviço remoto. Selecione outra versão e tente novamente.'
|
||||||
ErrVllmGB10ProOnly: 'Esta versão do vLLM está disponível apenas no 1Panel Professional Edition.'
|
ErrVllmGB10ProOnly: 'Esta versão do vLLM está disponível apenas no 1Panel Professional Edition.'
|
||||||
|
ErrAIGatewayDatabaseTypeMismatch: 'O AI Gateway só pode ser atualizado dentro do mesmo tipo de banco de dados (SQLite ou PostgreSQL).'
|
||||||
|
ErrAIGatewayRecoverDatabaseTypeMismatch: 'O backup do AI Gateway e o aplicativo de destino devem usar o mesmo tipo de banco de dados (SQLite ou PostgreSQL).'
|
||||||
|
ErrHermesUpgradeVersion: 'O Hermes Agent só pode ser atualizado para uma versão reconhecida mais recente permitida pela configuração de atualização entre versões principais. Para voltar a uma versão anterior, restaure um backup.'
|
||||||
ErrAppWarn: 'Status anormal, verifique o log'
|
ErrAppWarn: 'Status anormal, verifique o log'
|
||||||
ErrAppParamKey: 'O campo de parâmetro {{ .name }} está anormal'
|
ErrAppParamKey: 'O campo de parâmetro {{ .name }} está anormal'
|
||||||
ErrAppUpgrade: 'Falha na atualização do aplicativo {{ .name }} {{ .err }}'
|
ErrAppUpgrade: 'Falha na atualização do aplicativo {{ .name }} {{ .err }}'
|
||||||
@@ -300,6 +303,9 @@ BuildCache: 'Cache de construção'
|
|||||||
Volume: 'Volume de armazenamento'
|
Volume: 'Volume de armazenamento'
|
||||||
Network: 'Rede'
|
Network: 'Rede'
|
||||||
PruneStart: 'Limpando em andamento aguarde...'
|
PruneStart: 'Limpando em andamento aguarde...'
|
||||||
|
NetworkCleanupConnected: "Rede [{{ .name }}] ({{ .id }}) ignorada: há contêineres conectados ou a rede está em uso; não foi excluída"
|
||||||
|
NetworkCleanupSummary: "Limpeza de redes concluída: excluídas {{ .deleted }}, ignoradas {{ .skipped }}, falhas {{ .failed }}"
|
||||||
|
NetworkCleanupPartialFailure: "Não foi possível limpar algumas redes; consulte o log da tarefa"
|
||||||
ContainerBackupPrepare: 'Preparar área de backup do contêiner'
|
ContainerBackupPrepare: 'Preparar área de backup do contêiner'
|
||||||
ContainerBackupStop: 'Parar contêiner antes do backup'
|
ContainerBackupStop: 'Parar contêiner antes do backup'
|
||||||
ContainerBackupInspect: 'Fazer backup do inspect e rede do contêiner'
|
ContainerBackupInspect: 'Fazer backup do inspect e rede do contêiner'
|
||||||
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'O tipo ou nome do aplicativo não corresponde'
|
|||||||
# alerta
|
# alerta
|
||||||
ErrAlertMethodNotSupported: 'Esta versão não oferece suporte a este método de alerta'
|
ErrAlertMethodNotSupported: 'Esta versão não oferece suporte a este método de alerta'
|
||||||
ErrAlertSync: 'Erro de sincronização de informações de alarme, verifique e'
|
ErrAlertSync: 'Erro de sincronização de informações de alarme, verifique e'
|
||||||
|
ErrWebhookTestFailed: 'Verifique a configuração do Webhook. Consulte os logs do sistema para mais detalhes.'
|
||||||
|
|
||||||
# tarefa - runtime
|
# tarefa - runtime
|
||||||
ErrInstallExtension: 'Já existe uma tarefa de instalação em andamento, aguarde a conclusão da tarefa'
|
ErrInstallExtension: 'Já existe uma tarefa de instalação em andamento, aguarde a conclusão da tarefa'
|
||||||
|
|
||||||
# modelo de e-mail de alerta
|
# modelo de e-mail de alerta
|
||||||
PanelAlertTitle: 'Notificação de Alerta do Painel'
|
PanelAlertTitle: 'Notificação de Alerta do Painel'
|
||||||
TestAlertTitle: 'E-mail de Teste - Verificar Conectividade de E-mail'
|
TestAlertTitle: 'Notificação de teste do painel'
|
||||||
TestAlert: 'Este é um e-mail de teste para verificar se sua configuração de envio de e-mails está correta.'
|
TestAlertMessage: 'Esta é uma notificação de teste para verificar a configuração e a conectividade do canal de notificações.'
|
||||||
LicenseExpirationAlert: 'Seu Painel {{ .node }}{{ .ip }}, a licença expirará em {{ .day }} dias. Faça login no painel para obter detalhes.'
|
LicenseExpirationAlert: 'Seu Painel {{ .node }}{{ .ip }}, a licença expirará em {{ .day }} dias. Faça login no painel para obter detalhes.'
|
||||||
CronJobFailedAlert: 'Seu Painel {{ .node }}{{ .ip }}, a execução da tarefa agendada {{ .name }} falhou. Faça login no painel para obter detalhes.'
|
CronJobFailedAlert: 'Seu Painel {{ .node }}{{ .ip }}, a execução da tarefa agendada {{ .name }} falhou. Faça login no painel para obter detalhes.'
|
||||||
ClamAlert: 'Seu Painel {{ .node }}{{ .ip }}, a tarefa de verificação de vírus detectou {{ .num }} arquivos infectados. Faça login no painel para obter detalhes.'
|
ClamAlert: 'Seu Painel {{ .node }}{{ .ip }}, a tarefa de verificação de vírus detectou {{ .num }} arquivos infectados. Faça login no painel para obter detalhes.'
|
||||||
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "O encaminhamento IPv4 está desativado. Defina
|
|||||||
ErrFirewallRuleSavedApplyFailed: "A configuração da nova regra foi salva, mas não pôde ser aplicada ao firewall. Tente novamente por meio da sincronização: {{ .detail }}"
|
ErrFirewallRuleSavedApplyFailed: "A configuração da nova regra foi salva, mas não pôde ser aplicada ao firewall. Tente novamente por meio da sincronização: {{ .detail }}"
|
||||||
ErrFirewallRuleConflict: "Já existe uma regra UFW com as mesmas condições e uma ação diferente. Edite a regra existente."
|
ErrFirewallRuleConflict: "Já existe uma regra UFW com as mesmas condições e uma ação diferente. Edite a regra existente."
|
||||||
ErrForwardInterfaceNotFound: "A interface de rede {{ .name }} especificada na regra de encaminhamento não existe. Verifique o nome e tente novamente."
|
ErrForwardInterfaceNotFound: "A interface de rede {{ .name }} especificada na regra de encaminhamento não existe. Verifique o nome e tente novamente."
|
||||||
|
ErrIPv6RARisk: "O encaminhamento IPv6 foi bloqueado para evitar uma possível perda de conexão. Consulte a documentação e os logs do sistema."
|
||||||
|
ErrIPv6RACheckFailed: "O encaminhamento IPv6 foi bloqueado porque não foi possível verificar a configuração de rede. Consulte os logs do sistema."
|
||||||
|
|
||||||
ErrSSHPublicKey: "Informe uma chave pública SSH válida"
|
ErrSSHPublicKey: "Informe uma chave pública SSH válida"
|
||||||
ErrSSHPrivateKey: "Chave privada SSH inválida ou senha incorreta; chaves criptografadas exigem a senha original"
|
ErrSSHPrivateKey: "Chave privada SSH inválida ou senha incorreta; chaves criptografadas exigem a senha original"
|
||||||
|
|||||||
+11
-2
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Не удалось получить имя про
|
|||||||
ErrComposeNameInvalid: 'Недопустимое имя проекта Compose: оно должно начинаться со строчной буквы или цифры, содержать только строчные буквы, цифры, дефисы и подчёркивания и иметь длину от 1 до 256 символов.'
|
ErrComposeNameInvalid: 'Недопустимое имя проекта Compose: оно должно начинаться со строчной буквы или цифры, содержать только строчные буквы, цифры, дефисы и подчёркивания и иметь длину от 1 до 256 символов.'
|
||||||
ErrAppVersionUnavailable: 'Эта версия приложения удалена из удаленного сервиса. Выберите другую версию и повторите попытку.'
|
ErrAppVersionUnavailable: 'Эта версия приложения удалена из удаленного сервиса. Выберите другую версию и повторите попытку.'
|
||||||
ErrVllmGB10ProOnly: 'Эта версия vLLM доступна только в профессиональной редакции 1Panel.'
|
ErrVllmGB10ProOnly: 'Эта версия vLLM доступна только в профессиональной редакции 1Panel.'
|
||||||
|
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway можно обновлять только в пределах одного типа базы данных (SQLite или PostgreSQL).'
|
||||||
|
ErrAIGatewayRecoverDatabaseTypeMismatch: 'Резервная копия AI Gateway и целевое приложение должны использовать один и тот же тип базы данных (SQLite или PostgreSQL).'
|
||||||
|
ErrHermesUpgradeVersion: 'Hermes Agent можно обновить только до более нового распознанного выпуска, разрешённого настройкой обновления между основными версиями. Для возврата к старой версии восстановите резервную копию.'
|
||||||
ErrAppWarn: 'Ненормальное состояние, проверьте журнал'
|
ErrAppWarn: 'Ненормальное состояние, проверьте журнал'
|
||||||
ErrAppParamKey: 'Поле параметра {{ .name }} ненормально'
|
ErrAppParamKey: 'Поле параметра {{ .name }} ненормально'
|
||||||
ErrAppUpgrade: 'Обновление приложения {{ .name }} не удалось {{ .err }}'
|
ErrAppUpgrade: 'Обновление приложения {{ .name }} не удалось {{ .err }}'
|
||||||
@@ -300,6 +303,9 @@ BuildCache: 'Кэш сборки'
|
|||||||
Volume: 'Том хранилища'
|
Volume: 'Том хранилища'
|
||||||
Network: 'Сеть'
|
Network: 'Сеть'
|
||||||
PruneStart: 'Очистка выполняется, пожалуйста, подождите...'
|
PruneStart: 'Очистка выполняется, пожалуйста, подождите...'
|
||||||
|
NetworkCleanupConnected: "Сеть [{{ .name }}] ({{ .id }}) пропущена: подключены контейнеры или сеть используется; не удалена"
|
||||||
|
NetworkCleanupSummary: "Очистка сетей завершена: удалено {{ .deleted }}, пропущено {{ .skipped }}, ошибок {{ .failed }}"
|
||||||
|
NetworkCleanupPartialFailure: "Не удалось очистить некоторые сети; проверьте журнал задачи"
|
||||||
ContainerBackupPrepare: 'Подготовить рабочую область резервной копии контейнера'
|
ContainerBackupPrepare: 'Подготовить рабочую область резервной копии контейнера'
|
||||||
ContainerBackupStop: 'Остановить контейнер перед резервным копированием'
|
ContainerBackupStop: 'Остановить контейнер перед резервным копированием'
|
||||||
ContainerBackupInspect: 'Сделать резервную копию inspect и сети контейнера'
|
ContainerBackupInspect: 'Сделать резервную копию inspect и сети контейнера'
|
||||||
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'Тип или имя приложения не совп
|
|||||||
# оповещение
|
# оповещение
|
||||||
ErrAlertMethodNotSupported: 'Эта версия не поддерживает данный способ оповещения'
|
ErrAlertMethodNotSupported: 'Эта версия не поддерживает данный способ оповещения'
|
||||||
ErrAlertSync: 'Ошибка синхронизации информации о тревоге, проверьте и повторите попытку'
|
ErrAlertSync: 'Ошибка синхронизации информации о тревоге, проверьте и повторите попытку'
|
||||||
|
ErrWebhookTestFailed: 'Проверьте конфигурацию Webhook. Подробности доступны в системных журналах.'
|
||||||
|
|
||||||
# задача - среда выполнения
|
# задача - среда выполнения
|
||||||
ErrInstallExtension: 'Уже выполняется задача установки, подождите, пока задача завершится'
|
ErrInstallExtension: 'Уже выполняется задача установки, подождите, пока задача завершится'
|
||||||
|
|
||||||
# шаблон письма оповещения
|
# шаблон письма оповещения
|
||||||
PanelAlertTitle: 'Уведомление панели'
|
PanelAlertTitle: 'Уведомление панели'
|
||||||
TestAlertTitle: 'Тестовое письмо - проверка подключения'
|
TestAlertTitle: 'Тестовое уведомление панели'
|
||||||
TestAlert: 'Это тестовое письмо для проверки правильности настройки отправки писем.'
|
TestAlertMessage: 'Это тестовое уведомление для проверки настроек канала уведомлений и подключения.'
|
||||||
LicenseExpirationAlert: 'Ваш панель {{ .node }}{{ .ip }}, лицензия истечет через {{ .day }} дней. Войдите в панель для получения деталей.'
|
LicenseExpirationAlert: 'Ваш панель {{ .node }}{{ .ip }}, лицензия истечет через {{ .day }} дней. Войдите в панель для получения деталей.'
|
||||||
CronJobFailedAlert: 'Ваш панель {{ .node }}{{ .ip }}, выполнение запланированной задачи {{ .name }} не удалось. Войдите в панель для получения деталей.'
|
CronJobFailedAlert: 'Ваш панель {{ .node }}{{ .ip }}, выполнение запланированной задачи {{ .name }} не удалось. Войдите в панель для получения деталей.'
|
||||||
ClamAlert: 'Ваш панель {{ .node }}{{ .ip }}, задача сканирования на вирусы обнаружила {{ .num }} зараженных файлов. Войдите в панель для получения деталей.'
|
ClamAlert: 'Ваш панель {{ .node }}{{ .ip }}, задача сканирования на вирусы обнаружила {{ .num }} зараженных файлов. Войдите в панель для получения деталей.'
|
||||||
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. П
|
|||||||
ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}"
|
ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}"
|
||||||
ErrFirewallRuleConflict: "В UFW уже есть правило с такими же условиями и другим действием. Отредактируйте существующее правило."
|
ErrFirewallRuleConflict: "В UFW уже есть правило с такими же условиями и другим действием. Отредактируйте существующее правило."
|
||||||
ErrForwardInterfaceNotFound: "Сетевой интерфейс {{ .name }}, указанный в правиле перенаправления, не существует. Проверьте имя и повторите попытку."
|
ErrForwardInterfaceNotFound: "Сетевой интерфейс {{ .name }}, указанный в правиле перенаправления, не существует. Проверьте имя и повторите попытку."
|
||||||
|
ErrIPv6RARisk: "Включение переадресации IPv6 заблокировано из-за риска потери связи. См. документацию и системные журналы."
|
||||||
|
ErrIPv6RACheckFailed: "Включение переадресации IPv6 заблокировано: не удалось проверить настройки сети. Подробности в системных журналах."
|
||||||
|
|
||||||
ErrSSHPublicKey: "Введите действительный открытый ключ SSH"
|
ErrSSHPublicKey: "Введите действительный открытый ключ SSH"
|
||||||
ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза"
|
ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза"
|
||||||
|
|||||||
+11
-2
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Docker Compose yapılandırmasından veya dosyanın
|
|||||||
ErrComposeNameInvalid: 'Geçersiz Compose proje adı: küçük harf veya rakamla başlamalı, yalnızca küçük harf, rakam, kısa çizgi ve alt çizgi içermeli ve 1-256 karakter uzunluğunda olmalıdır.'
|
ErrComposeNameInvalid: 'Geçersiz Compose proje adı: küçük harf veya rakamla başlamalı, yalnızca küçük harf, rakam, kısa çizgi ve alt çizgi içermeli ve 1-256 karakter uzunluğunda olmalıdır.'
|
||||||
ErrAppVersionUnavailable: 'Bu uygulama sürümü uzak hizmetten kaldırıldı. Lütfen başka bir sürüm seçip tekrar deneyin.'
|
ErrAppVersionUnavailable: 'Bu uygulama sürümü uzak hizmetten kaldırıldı. Lütfen başka bir sürüm seçip tekrar deneyin.'
|
||||||
ErrVllmGB10ProOnly: 'Bu vLLM sürümü yalnızca 1Panel Professional Edition sürümünde kullanılabilir.'
|
ErrVllmGB10ProOnly: 'Bu vLLM sürümü yalnızca 1Panel Professional Edition sürümünde kullanılabilir.'
|
||||||
|
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway yalnızca aynı veritabanı türü (SQLite veya PostgreSQL) içinde yükseltilebilir.'
|
||||||
|
ErrAIGatewayRecoverDatabaseTypeMismatch: 'AI Gateway yedeği ve hedef uygulama aynı veritabanı türünü (SQLite veya PostgreSQL) kullanmalıdır.'
|
||||||
|
ErrHermesUpgradeVersion: 'Hermes Agent yalnızca ana sürümler arası yükseltme ayarının izin verdiği, tanınan daha yeni bir sürüme yükseltilebilir. Eski bir sürüme dönmek için yedekten geri yükleyin.'
|
||||||
ErrAppWarn: 'Anormal durum, lütfen günlüğü kontrol edin'
|
ErrAppWarn: 'Anormal durum, lütfen günlüğü kontrol edin'
|
||||||
ErrAppParamKey: 'Parametre {{ .name }} alanı anormal'
|
ErrAppParamKey: 'Parametre {{ .name }} alanı anormal'
|
||||||
ErrAppUpgrade: 'Uygulama {{ .name }} yükseltmesi başarısız {{ .err }}'
|
ErrAppUpgrade: 'Uygulama {{ .name }} yükseltmesi başarısız {{ .err }}'
|
||||||
@@ -300,6 +303,9 @@ BuildCache: 'Derleme önbelleği'
|
|||||||
Volume: 'Depolama hacmi'
|
Volume: 'Depolama hacmi'
|
||||||
Network: 'Ağ'
|
Network: 'Ağ'
|
||||||
PruneStart: 'Temizlik devam ediyor, lütfen bekleyin...'
|
PruneStart: 'Temizlik devam ediyor, lütfen bekleyin...'
|
||||||
|
NetworkCleanupConnected: "Ağ [{{ .name }}] ({{ .id }}) atlandı: bağlı konteynerler var veya ağ kullanımda; silinmedi"
|
||||||
|
NetworkCleanupSummary: "Ağ temizliği tamamlandı: silinen {{ .deleted }}, atlanan {{ .skipped }}, başarısız {{ .failed }}"
|
||||||
|
NetworkCleanupPartialFailure: "Bazı ağlar temizlenemedi; görev günlüğünü kontrol edin"
|
||||||
ContainerBackupPrepare: 'Konteyner yedekleme çalışma alanını hazırla'
|
ContainerBackupPrepare: 'Konteyner yedekleme çalışma alanını hazırla'
|
||||||
ContainerBackupStop: 'Yedeklemeden önce konteyneri durdur'
|
ContainerBackupStop: 'Yedeklemeden önce konteyneri durdur'
|
||||||
ContainerBackupInspect: 'Konteyner inspect ve ağ bilgisini yedekle'
|
ContainerBackupInspect: 'Konteyner inspect ve ağ bilgisini yedekle'
|
||||||
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'Uygulama türü veya adı eşleşmiyor'
|
|||||||
# uyarı
|
# uyarı
|
||||||
ErrAlertMethodNotSupported: 'Bu sürüm bu uyarı yöntemini desteklemiyor'
|
ErrAlertMethodNotSupported: 'Bu sürüm bu uyarı yöntemini desteklemiyor'
|
||||||
ErrAlertSync: 'Alarm bilgisi senkronizasyon hatası, lütfen kontrol edin ve tekrar deneyin'
|
ErrAlertSync: 'Alarm bilgisi senkronizasyon hatası, lütfen kontrol edin ve tekrar deneyin'
|
||||||
|
ErrWebhookTestFailed: 'Lütfen Webhook yapılandırmasını kontrol edin. Ayrıntılar için sistem günlüklerine bakın.'
|
||||||
|
|
||||||
# görev - çalışma zamanı
|
# görev - çalışma zamanı
|
||||||
ErrInstallExtension: 'Zaten devam eden bir kurulum görevi var, görevin bitmesini bekleyin'
|
ErrInstallExtension: 'Zaten devam eden bir kurulum görevi var, görevin bitmesini bekleyin'
|
||||||
|
|
||||||
# uyarı e-posta şablonu
|
# uyarı e-posta şablonu
|
||||||
PanelAlertTitle: 'Panel Uyarı Bildirimi'
|
PanelAlertTitle: 'Panel Uyarı Bildirimi'
|
||||||
TestAlertTitle: 'Test E-postası - E-posta Bağlantısını Doğrula'
|
TestAlertTitle: 'Panel Test Bildirimi'
|
||||||
TestAlert: 'Bu, e-posta gönderim ayarlarınızın doğru yapılandırıldığını doğrulamak için gönderilen bir test e-postasıdır.'
|
TestAlertMessage: 'Bu, bildirim kanalı yapılandırmasını ve bağlantısını doğrulamak için gönderilen bir test bildirimidir.'
|
||||||
LicenseExpirationAlert: 'Paneliniz {{ .node }}{{ .ip }}, lisans {{ .day }} gün içinde süresi dolacak. Detaylar için paneline giriş yapın.'
|
LicenseExpirationAlert: 'Paneliniz {{ .node }}{{ .ip }}, lisans {{ .day }} gün içinde süresi dolacak. Detaylar için paneline giriş yapın.'
|
||||||
CronJobFailedAlert: 'Paneliniz {{ .node }}{{ .ip }}, zamanlanmış görev {{ .name }} çalıştırma başarısız oldu. Detaylar için paneline giriş yapın.'
|
CronJobFailedAlert: 'Paneliniz {{ .node }}{{ .ip }}, zamanlanmış görev {{ .name }} çalıştırma başarısız oldu. Detaylar için paneline giriş yapın.'
|
||||||
ClamAlert: 'Paneliniz {{ .node }}{{ .ip }}, virüs tarama görevi {{ .num }} enfekte dosya tespit etti. Detaylar için paneline giriş yapın.'
|
ClamAlert: 'Paneliniz {{ .node }}{{ .ip }}, virüs tarama görevi {{ .num }} enfekte dosya tespit etti. Detaylar için paneline giriş yapın.'
|
||||||
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 yönlendirmesi devre dışı. Docker güv
|
|||||||
ErrFirewallRuleSavedApplyFailed: "Yeni kural yapılandırması kaydedildi, ancak güvenlik duvarına uygulanamadı. Eşitleme yaparak yeniden deneyin: {{ .detail }}"
|
ErrFirewallRuleSavedApplyFailed: "Yeni kural yapılandırması kaydedildi, ancak güvenlik duvarına uygulanamadı. Eşitleme yaparak yeniden deneyin: {{ .detail }}"
|
||||||
ErrFirewallRuleConflict: "UFW içinde aynı eşleşme koşullarına sahip, farklı bir eylem uygulayan bir kural zaten var. Mevcut kuralı düzenleyin."
|
ErrFirewallRuleConflict: "UFW içinde aynı eşleşme koşullarına sahip, farklı bir eylem uygulayan bir kural zaten var. Mevcut kuralı düzenleyin."
|
||||||
ErrForwardInterfaceNotFound: "Yönlendirme kuralında belirtilen {{ .name }} ağ arayüzü mevcut değil. Arayüz adını kontrol edip yeniden deneyin."
|
ErrForwardInterfaceNotFound: "Yönlendirme kuralında belirtilen {{ .name }} ağ arayüzü mevcut değil. Arayüz adını kontrol edip yeniden deneyin."
|
||||||
|
ErrIPv6RARisk: "Ağ bağlantısının kesilme riski nedeniyle IPv6 yönlendirmesi engellendi. Belgeleri ve sistem günlüklerini inceleyin."
|
||||||
|
ErrIPv6RACheckFailed: "Ağ yapılandırması denetlenemediği için IPv6 yönlendirmesi engellendi. Ayrıntılar için sistem günlüklerini inceleyin."
|
||||||
|
|
||||||
ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin"
|
ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin"
|
||||||
ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir"
|
ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir"
|
||||||
|
|||||||
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: '無法從 Docker Compose 設定或檔案父目錄
|
|||||||
ErrComposeNameInvalid: 'Compose 專案名稱格式錯誤:必須以小寫字母或數字開頭,僅支援小寫字母、數字、- 和 _,長度為 1-256 個字元'
|
ErrComposeNameInvalid: 'Compose 專案名稱格式錯誤:必須以小寫字母或數字開頭,僅支援小寫字母、數字、- 和 _,長度為 1-256 個字元'
|
||||||
ErrAppVersionUnavailable: '目前應用程式版本已從遠端服務下架,請選擇其他版本後重試'
|
ErrAppVersionUnavailable: '目前應用程式版本已從遠端服務下架,請選擇其他版本後重試'
|
||||||
ErrVllmGB10ProOnly: '此 vLLM 版本僅支援 1Panel 專業版'
|
ErrVllmGB10ProOnly: '此 vLLM 版本僅支援 1Panel 專業版'
|
||||||
|
ErrAIGatewayDatabaseTypeMismatch: 'AI 閘道只能在相同資料庫類型(SQLite 或 PostgreSQL)之間升級。'
|
||||||
|
ErrAIGatewayRecoverDatabaseTypeMismatch: 'AI 閘道備份與目標應用程式的資料庫類型(SQLite 或 PostgreSQL)必須一致,無法跨資料庫類型還原。'
|
||||||
|
ErrHermesUpgradeVersion: 'Hermes Agent 只能升級至可辨識且符合跨版本升級設定的較新版本。如需還原舊版本,請使用備份還原。'
|
||||||
ErrAppWarn: '狀態異常,請檢視日誌'
|
ErrAppWarn: '狀態異常,請檢視日誌'
|
||||||
ErrAppParamKey: '參數{{ .name }} 欄位異常'
|
ErrAppParamKey: '參數{{ .name }} 欄位異常'
|
||||||
ErrAppUpgrade: '應用程式{{ .name }} 升級失敗{{ .err }}'
|
ErrAppUpgrade: '應用程式{{ .name }} 升級失敗{{ .err }}'
|
||||||
@@ -300,6 +303,9 @@ BuildCache: '構建快取'
|
|||||||
Volume: '磁碟區'
|
Volume: '磁碟區'
|
||||||
Network: '網路'
|
Network: '網路'
|
||||||
PruneStart: '清理中,請稍候...'
|
PruneStart: '清理中,請稍候...'
|
||||||
|
NetworkCleanupConnected: "跳過網路 [{{ .name }}] ({{ .id }}):仍有容器連接或正在使用,未刪除"
|
||||||
|
NetworkCleanupSummary: "網路清理完成:已刪除 {{ .deleted }},已跳過 {{ .skipped }},失敗 {{ .failed }}"
|
||||||
|
NetworkCleanupPartialFailure: "部分網路清理失敗,請查看任務日誌"
|
||||||
ContainerBackupPrepare: '準備容器備份工作目錄'
|
ContainerBackupPrepare: '準備容器備份工作目錄'
|
||||||
ContainerBackupStop: '備份前停止容器'
|
ContainerBackupStop: '備份前停止容器'
|
||||||
ContainerBackupInspect: '備份容器 Inspect 與網路資訊'
|
ContainerBackupInspect: '備份容器 Inspect 與網路資訊'
|
||||||
@@ -628,14 +634,15 @@ AppAttributesNotMatch: '應用程式類型或名稱不一致'
|
|||||||
# 告警
|
# 告警
|
||||||
ErrAlertMethodNotSupported: '當前版本不支援此告警方式'
|
ErrAlertMethodNotSupported: '當前版本不支援此告警方式'
|
||||||
ErrAlertSync: '警告訊息同步錯誤,請重試。'
|
ErrAlertSync: '警告訊息同步錯誤,請重試。'
|
||||||
|
ErrWebhookTestFailed: '請檢查 Webhook 設定,詳細原因請查看系統日誌'
|
||||||
|
|
||||||
# 任務 - 執行環境
|
# 任務 - 執行環境
|
||||||
ErrInstallExtension: '已有安裝任務正在進行,請等待任務結束'
|
ErrInstallExtension: '已有安裝任務正在進行,請等待任務結束'
|
||||||
|
|
||||||
# 告警郵件範本
|
# 告警郵件範本
|
||||||
PanelAlertTitle: '面板警示通知'
|
PanelAlertTitle: '面板警示通知'
|
||||||
TestAlertTitle: '測試郵件 - 驗證信箱連通性'
|
TestAlertTitle: '面板測試通知'
|
||||||
TestAlert: '這是一封測試郵件,用於確認您的郵件發送設定是否正確。'
|
TestAlertMessage: '這是一則測試通知,用於驗證通知管道設定及連通性。'
|
||||||
LicenseExpirationAlert: '您的 {{ .node }}{{ .ip }} 面板,授權將於 {{ .day }} 天後到期,詳情請登入面板檢視。'
|
LicenseExpirationAlert: '您的 {{ .node }}{{ .ip }} 面板,授權將於 {{ .day }} 天後到期,詳情請登入面板檢視。'
|
||||||
CronJobFailedAlert: '您的 {{ .node }}{{ .ip }} 面板,排程任務{{ .name }}執行失敗,詳情請登入面板檢視。'
|
CronJobFailedAlert: '您的 {{ .node }}{{ .ip }} 面板,排程任務{{ .name }}執行失敗,詳情請登入面板檢視。'
|
||||||
ClamAlert: '您的 {{ .node }}{{ .ip }} 面板,病毒掃描任務偵測到 {{ .num }} 個感染檔案,詳情請登入面板檢視。'
|
ClamAlert: '您的 {{ .node }}{{ .ip }} 面板,病毒掃描任務偵測到 {{ .num }} 個感染檔案,詳情請登入面板檢視。'
|
||||||
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv
|
|||||||
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
|
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
|
||||||
ErrFirewallRuleConflict: "UFW 中已存在比對條件相同但動作不同的規則,請編輯既有規則。"
|
ErrFirewallRuleConflict: "UFW 中已存在比對條件相同但動作不同的規則,請編輯既有規則。"
|
||||||
ErrForwardInterfaceNotFound: "轉發規則指定的網卡 {{ .name }} 不存在,請檢查網卡名稱後重試。"
|
ErrForwardInterfaceNotFound: "轉發規則指定的網卡 {{ .name }} 不存在,請檢查網卡名稱後重試。"
|
||||||
|
ErrIPv6RARisk: "IPv6 轉送可能導致網路中斷,已阻止啟用。請參閱文件,詳情見系統日誌。"
|
||||||
|
ErrIPv6RACheckFailed: "無法檢查 IPv6 網路設定,已阻止啟用轉送。詳情見系統日誌。"
|
||||||
|
|
||||||
ErrSSHPublicKey: "請輸入有效的 SSH 公鑰"
|
ErrSSHPublicKey: "請輸入有效的 SSH 公鑰"
|
||||||
ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼"
|
ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼"
|
||||||
|
|||||||
+11
-2
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: "无法从 Docker Compose 配置或文件父目录
|
|||||||
ErrComposeNameInvalid: "Compose 项目名称格式错误:必须以小写字母或数字开头,仅支持小写字母、数字、- 和 _,长度为 1-256 个字符"
|
ErrComposeNameInvalid: "Compose 项目名称格式错误:必须以小写字母或数字开头,仅支持小写字母、数字、- 和 _,长度为 1-256 个字符"
|
||||||
ErrAppVersionUnavailable: "当前应用版本已从远程服务下架,请选择其他版本后重试"
|
ErrAppVersionUnavailable: "当前应用版本已从远程服务下架,请选择其他版本后重试"
|
||||||
ErrVllmGB10ProOnly: "该 vLLM 版本仅支持 1Panel 专业版"
|
ErrVllmGB10ProOnly: "该 vLLM 版本仅支持 1Panel 专业版"
|
||||||
|
ErrAIGatewayDatabaseTypeMismatch: 'AI 网关只能在相同数据库类型(SQLite 或 PostgreSQL)之间升级。'
|
||||||
|
ErrAIGatewayRecoverDatabaseTypeMismatch: 'AI 网关备份与目标应用的数据库类型(SQLite 或 PostgreSQL)必须一致,无法跨数据库类型恢复。'
|
||||||
|
ErrHermesUpgradeVersion: 'Hermes Agent 只能升级到可识别且符合跨版本升级设置的更新版本。如需恢复旧版本,请使用备份恢复。'
|
||||||
ErrAppWarn: "状态异常,请查看日志"
|
ErrAppWarn: "状态异常,请查看日志"
|
||||||
ErrAppParamKey: "参数 {{ .name }} 字段异常"
|
ErrAppParamKey: "参数 {{ .name }} 字段异常"
|
||||||
ErrAppUpgrade: "应用 {{ .name }} 升级失败 {{ .err }}"
|
ErrAppUpgrade: "应用 {{ .name }} 升级失败 {{ .err }}"
|
||||||
@@ -300,6 +303,9 @@ BuildCache: "构建缓存"
|
|||||||
Volume: "存储卷"
|
Volume: "存储卷"
|
||||||
Network: "网络"
|
Network: "网络"
|
||||||
PruneStart: "清理中,请稍候..."
|
PruneStart: "清理中,请稍候..."
|
||||||
|
NetworkCleanupConnected: "跳过网络 [{{ .name }}] ({{ .id }}):仍有容器连接或正在使用,未删除"
|
||||||
|
NetworkCleanupSummary: "网络清理完成:已删除 {{ .deleted }},已跳过 {{ .skipped }},失败 {{ .failed }}"
|
||||||
|
NetworkCleanupPartialFailure: "部分网络清理失败,请查看任务日志"
|
||||||
ContainerBackupPrepare: "准备容器备份工作目录"
|
ContainerBackupPrepare: "准备容器备份工作目录"
|
||||||
ContainerBackupStop: "备份前停止容器"
|
ContainerBackupStop: "备份前停止容器"
|
||||||
ContainerBackupInspect: "备份容器 Inspect 与网络信息"
|
ContainerBackupInspect: "备份容器 Inspect 与网络信息"
|
||||||
@@ -628,14 +634,15 @@ AppAttributesNotMatch: "应用类型或名称不一致"
|
|||||||
# 告警
|
# 告警
|
||||||
ErrAlertMethodNotSupported: "当前版本不支持此告警方式"
|
ErrAlertMethodNotSupported: "当前版本不支持此告警方式"
|
||||||
ErrAlertSync: "告警信息同步错误,请检查后重试"
|
ErrAlertSync: "告警信息同步错误,请检查后重试"
|
||||||
|
ErrWebhookTestFailed: "请检查 Webhook 配置,详细原因请查看系统日志"
|
||||||
|
|
||||||
# 任务 - 运行环境
|
# 任务 - 运行环境
|
||||||
ErrInstallExtension: "已有安装任务正在进行,请等待任务结束"
|
ErrInstallExtension: "已有安装任务正在进行,请等待任务结束"
|
||||||
|
|
||||||
# 告警邮件模板
|
# 告警邮件模板
|
||||||
PanelAlertTitle: "面板告警通知"
|
PanelAlertTitle: "面板告警通知"
|
||||||
TestAlertTitle: "测试邮件 - 验证邮箱连通性"
|
TestAlertTitle: "面板测试通知"
|
||||||
TestAlert: "这是一封测试邮件,用于验证邮箱发件配置。"
|
TestAlertMessage: "这是一条测试通知,用于验证通知渠道配置及连通性。"
|
||||||
LicenseExpirationAlert: "您的 {{ .node }}{{ .ip }} 面板,许可证将在 {{ .day }} 天后到期,请登录面板查看详情。"
|
LicenseExpirationAlert: "您的 {{ .node }}{{ .ip }} 面板,许可证将在 {{ .day }} 天后到期,请登录面板查看详情。"
|
||||||
CronJobFailedAlert: "您的 {{ .node }}{{ .ip }} 面板,计划任务-{{ .name }}执行失败,请登录面板查看详情。"
|
CronJobFailedAlert: "您的 {{ .node }}{{ .ip }} 面板,计划任务-{{ .name }}执行失败,请登录面板查看详情。"
|
||||||
ClamAlert: "您的 {{ .node }}{{ .ip }} 面板,病毒扫描任务发现 {{ .num }} 个感染文件,请登录面板查看详情。"
|
ClamAlert: "您的 {{ .node }}{{ .ip }} 面板,病毒扫描任务发现 {{ .num }} 个感染文件,请登录面板查看详情。"
|
||||||
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.i
|
|||||||
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
|
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
|
||||||
ErrFirewallRuleConflict: "UFW 中已存在匹配条件相同但动作不同的规则,请编辑已有规则。"
|
ErrFirewallRuleConflict: "UFW 中已存在匹配条件相同但动作不同的规则,请编辑已有规则。"
|
||||||
ErrForwardInterfaceNotFound: "转发规则指定的网卡 {{ .name }} 不存在,请检查网卡名称后重试。"
|
ErrForwardInterfaceNotFound: "转发规则指定的网卡 {{ .name }} 不存在,请检查网卡名称后重试。"
|
||||||
|
ErrIPv6RARisk: "IPv6 转发存在断网风险,已阻止启用。请参阅文档,详情见系统日志。"
|
||||||
|
ErrIPv6RACheckFailed: "无法检查 IPv6 网络配置,已阻止启用转发。详情见系统日志。"
|
||||||
|
|
||||||
ErrSSHPublicKey: "请输入有效的 SSH 公钥"
|
ErrSSHPublicKey: "请输入有效的 SSH 公钥"
|
||||||
ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码"
|
ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码"
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import (
|
|||||||
func Init() {
|
func Init() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
defer initDockerPortGuard(ctx)
|
defer initDockerPortGuard(ctx)
|
||||||
|
if err := service.SyncPanelPortWhitelist(); err != nil {
|
||||||
|
global.LOG.Warnf("synchronize panel port whitelist on startup failed, err: %v", err)
|
||||||
|
}
|
||||||
if err := initForwardingRules(ctx); err != nil {
|
if err := initForwardingRules(ctx); err != nil {
|
||||||
global.LOG.Warnf("restore forwarding rules from file failed, err: %v", err)
|
global.LOG.Warnf("restore forwarding rules from file failed, err: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ func agentDBMigrations() []*gormigrate.Migration {
|
|||||||
migrations.AddAcceleratorMetrics,
|
migrations.AddAcceleratorMetrics,
|
||||||
migrations.AddVLLMMonitor,
|
migrations.AddVLLMMonitor,
|
||||||
migrations.AddMonitorSettings,
|
migrations.AddMonitorSettings,
|
||||||
|
migrations.AddVLLMRequestMetrics,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1874,3 +1874,10 @@ var AddMonitorSettings = &gormigrate.Migration{
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var AddVLLMRequestMetrics = &gormigrate.Migration{
|
||||||
|
ID: "20261010-vllm-request-metrics",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package vllm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HistoryAccumulator struct {
|
||||||
|
histograms map[string]histogram
|
||||||
|
prefixHits float64
|
||||||
|
prefixQueries float64
|
||||||
|
finishReasons map[string]float64
|
||||||
|
finishSeconds map[string]float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *HistoryAccumulator) Add(histograms, requests, aggregation, group string) error {
|
||||||
|
if histograms != "" {
|
||||||
|
var sample map[string]histogram
|
||||||
|
if aggregation == "max" || group == "requests" {
|
||||||
|
var distributions struct {
|
||||||
|
Prompt *histogram `json:"request_prompt_tokens"`
|
||||||
|
Generation *histogram `json:"request_generation_tokens"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(histograms), &distributions); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sample = map[string]histogram{}
|
||||||
|
if distributions.Prompt != nil {
|
||||||
|
sample["request_prompt_tokens"] = *distributions.Prompt
|
||||||
|
}
|
||||||
|
if distributions.Generation != nil {
|
||||||
|
sample["request_generation_tokens"] = *distributions.Generation
|
||||||
|
}
|
||||||
|
} else if group == "latency" {
|
||||||
|
var latency struct {
|
||||||
|
FirstToken *histogram `json:"time_to_first_token_seconds"`
|
||||||
|
InterToken *histogram `json:"inter_token_latency_seconds"`
|
||||||
|
Request *histogram `json:"e2e_request_latency_seconds"`
|
||||||
|
Prefill *histogram `json:"request_prefill_time_seconds"`
|
||||||
|
Decode *histogram `json:"request_decode_time_seconds"`
|
||||||
|
Queue *histogram `json:"request_queue_time_seconds"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(histograms), &latency); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sample = map[string]histogram{}
|
||||||
|
for name, item := range map[string]*histogram{
|
||||||
|
"time_to_first_token_seconds": latency.FirstToken,
|
||||||
|
"inter_token_latency_seconds": latency.InterToken,
|
||||||
|
"e2e_request_latency_seconds": latency.Request,
|
||||||
|
"request_prefill_time_seconds": latency.Prefill,
|
||||||
|
"request_decode_time_seconds": latency.Decode,
|
||||||
|
"request_queue_time_seconds": latency.Queue,
|
||||||
|
} {
|
||||||
|
if item != nil {
|
||||||
|
sample[name] = *item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if err := json.Unmarshal([]byte(histograms), &sample); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if a.histograms == nil {
|
||||||
|
a.histograms = map[string]histogram{}
|
||||||
|
}
|
||||||
|
for name, item := range sample {
|
||||||
|
switch name {
|
||||||
|
case "request_prefill_time_seconds", "request_decode_time_seconds", "request_queue_time_seconds":
|
||||||
|
item.Buckets = nil
|
||||||
|
}
|
||||||
|
total, exists := a.histograms[name]
|
||||||
|
if !exists {
|
||||||
|
a.histograms[name] = item
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total.Seconds += item.Seconds
|
||||||
|
total.Sum += item.Sum
|
||||||
|
total.Count += item.Count
|
||||||
|
if len(total.Buckets) != len(item.Buckets) {
|
||||||
|
total.Buckets = nil
|
||||||
|
}
|
||||||
|
for bound, count := range total.Buckets {
|
||||||
|
addition, ok := item.Buckets[bound]
|
||||||
|
if !ok {
|
||||||
|
total.Buckets = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
total.Buckets[bound] = count + addition
|
||||||
|
}
|
||||||
|
a.histograms[name] = total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if requests == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if group == "overview" {
|
||||||
|
var prefix struct {
|
||||||
|
Seconds float64 `json:"seconds"`
|
||||||
|
Hits *float64 `json:"prefixHits"`
|
||||||
|
Queries *float64 `json:"prefixQueries"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(requests), &prefix); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if prefix.Seconds > 0 && prefix.Hits != nil && prefix.Queries != nil {
|
||||||
|
a.prefixHits += *prefix.Hits
|
||||||
|
a.prefixQueries += *prefix.Queries
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var sample requestMetrics
|
||||||
|
if err := json.Unmarshal([]byte(requests), &sample); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sample.Seconds <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if group == "" && aggregation != "max" && sample.PrefixHits != nil && sample.PrefixQueries != nil {
|
||||||
|
a.prefixHits += *sample.PrefixHits
|
||||||
|
a.prefixQueries += *sample.PrefixQueries
|
||||||
|
}
|
||||||
|
for reason, count := range sample.FinishReasons {
|
||||||
|
if a.finishReasons == nil {
|
||||||
|
a.finishReasons = map[string]float64{}
|
||||||
|
}
|
||||||
|
if aggregation == "max" {
|
||||||
|
a.finishReasons[reason] = math.Max(a.finishReasons[reason], count/sample.Seconds)
|
||||||
|
} else {
|
||||||
|
if a.finishSeconds == nil {
|
||||||
|
a.finishSeconds = map[string]float64{}
|
||||||
|
}
|
||||||
|
a.finishReasons[reason] += count
|
||||||
|
a.finishSeconds[reason] += sample.Seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *HistoryAccumulator) Apply(result *model.MonitorVLLM, aggregation string) {
|
||||||
|
applyHistograms(result, a.histograms, aggregation != "max")
|
||||||
|
for reason, count := range a.finishReasons {
|
||||||
|
if result.FinishReasons == nil {
|
||||||
|
result.FinishReasons = map[string]float64{}
|
||||||
|
}
|
||||||
|
if aggregation == "max" {
|
||||||
|
result.FinishReasons[reason] = count
|
||||||
|
} else {
|
||||||
|
result.FinishReasons[reason] = count / a.finishSeconds[reason]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if aggregation != "max" && a.prefixQueries > 0 {
|
||||||
|
rate := a.prefixHits / a.prefixQueries * 100
|
||||||
|
result.PrefixCacheHitRate = &rate
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
type Metrics map[string]map[string]float64
|
type Metrics map[string]map[string]float64
|
||||||
|
|
||||||
type histogram struct {
|
type histogram struct {
|
||||||
|
Seconds float64 `json:"seconds,omitempty"`
|
||||||
Sum float64 `json:"sum"`
|
Sum float64 `json:"sum"`
|
||||||
Count float64 `json:"count"`
|
Count float64 `json:"count"`
|
||||||
Buckets map[string]float64 `json:"buckets,omitempty"`
|
Buckets map[string]float64 `json:"buckets,omitempty"`
|
||||||
@@ -37,14 +38,16 @@ func Parse(reader io.Reader) (Metrics, error) {
|
|||||||
}
|
}
|
||||||
name := strings.TrimPrefix(match[1], "vllm:")
|
name := strings.TrimPrefix(match[1], "vllm:")
|
||||||
switch name {
|
switch name {
|
||||||
case "num_requests_running", "num_requests_waiting", "kv_cache_usage_perc", "gpu_cache_usage_perc", "prompt_tokens_total", "generation_tokens_total", "request_success_total":
|
case "request_prefill_time_seconds_bucket", "request_decode_time_seconds_bucket", "request_queue_time_seconds_bucket":
|
||||||
|
continue
|
||||||
|
case "num_requests_running", "num_requests_waiting", "kv_cache_usage_perc", "gpu_cache_usage_perc", "prompt_tokens_total", "generation_tokens_total", "request_success_total", "prefix_cache_hits_total", "prefix_cache_queries_total":
|
||||||
default:
|
default:
|
||||||
base := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(name, "_sum"), "_count"), "_bucket")
|
base := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(name, "_sum"), "_count"), "_bucket")
|
||||||
if base == name {
|
if base == name {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch base {
|
switch base {
|
||||||
case "time_to_first_token_seconds", "time_per_output_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds":
|
case "time_to_first_token_seconds", "time_per_output_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds", "request_queue_time_seconds", "request_prompt_tokens", "request_generation_tokens":
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -117,7 +120,7 @@ func Calculate(current, previous Metrics, elapsed float64) (model.MonitorVLLM, e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
histograms := map[string]histogram{}
|
histograms := map[string]histogram{}
|
||||||
for _, name := range []string{"time_to_first_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds"} {
|
for _, name := range []string{"time_to_first_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds", "request_queue_time_seconds", "request_prompt_tokens", "request_generation_tokens"} {
|
||||||
source := name
|
source := name
|
||||||
if name == "inter_token_latency_seconds" && len(current[name+"_count"]) == 0 {
|
if name == "inter_token_latency_seconds" && len(current[name+"_count"]) == 0 {
|
||||||
source = "time_per_output_token_seconds"
|
source = "time_per_output_token_seconds"
|
||||||
@@ -127,9 +130,21 @@ func Calculate(current, previous Metrics, elapsed float64) (model.MonitorVLLM, e
|
|||||||
if sum == nil || count == nil {
|
if sum == nil || count == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
histograms[name] = histogram{Sum: *sum, Count: *count, Buckets: bucketDeltas(current[source+"_bucket"], previous[source+"_bucket"], *count)}
|
item := histogram{Sum: *sum, Count: *count}
|
||||||
|
switch name {
|
||||||
|
case "request_prefill_time_seconds", "request_decode_time_seconds", "request_queue_time_seconds":
|
||||||
|
default:
|
||||||
|
item.Buckets = bucketDeltas(current[source+"_bucket"], previous[source+"_bucket"], *count)
|
||||||
|
}
|
||||||
|
if name == "request_prompt_tokens" || name == "request_generation_tokens" {
|
||||||
|
item.Seconds = elapsed
|
||||||
|
}
|
||||||
|
histograms[name] = item
|
||||||
|
}
|
||||||
|
applyHistograms(&result, histograms, true)
|
||||||
|
if err := calculateRequestMetrics(current, previous, elapsed, &result); err != nil {
|
||||||
|
return model.MonitorVLLM{}, err
|
||||||
}
|
}
|
||||||
applyHistograms(&result, histograms)
|
|
||||||
encoded, err := json.Marshal(histograms)
|
encoded, err := json.Marshal(histograms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.MonitorVLLM{}, err
|
return model.MonitorVLLM{}, err
|
||||||
@@ -138,43 +153,24 @@ func Calculate(current, previous Metrics, elapsed float64) (model.MonitorVLLM, e
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AggregateHistograms(encoded string, result *model.MonitorVLLM) error {
|
func applyHistograms(result *model.MonitorVLLM, histograms map[string]histogram, includeMeans bool) {
|
||||||
var samples []map[string]histogram
|
for name, target := range map[string]*map[string]float64{
|
||||||
if err := json.Unmarshal([]byte(encoded), &samples); err != nil {
|
"request_prompt_tokens": &result.PromptDistribution,
|
||||||
return err
|
"request_generation_tokens": &result.GenerationDistribution,
|
||||||
}
|
} {
|
||||||
merged := map[string]histogram{}
|
item := histograms[name]
|
||||||
for _, sample := range samples {
|
if item.Seconds <= 0 || len(item.Buckets) == 0 {
|
||||||
for name, item := range sample {
|
|
||||||
if item.Count == 0 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
total, exists := merged[name]
|
result.IntervalSeconds = math.Max(result.IntervalSeconds, item.Seconds)
|
||||||
if !exists {
|
*target = make(map[string]float64, len(item.Buckets))
|
||||||
merged[name] = item
|
for bound, count := range item.Buckets {
|
||||||
continue
|
(*target)[bound] = count / item.Seconds
|
||||||
}
|
|
||||||
total.Sum += item.Sum
|
|
||||||
total.Count += item.Count
|
|
||||||
if len(total.Buckets) != len(item.Buckets) {
|
|
||||||
total.Buckets = nil
|
|
||||||
}
|
|
||||||
for bound, count := range total.Buckets {
|
|
||||||
addition, ok := item.Buckets[bound]
|
|
||||||
if !ok {
|
|
||||||
total.Buckets = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
total.Buckets[bound] = count + addition
|
|
||||||
}
|
|
||||||
merged[name] = total
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applyHistograms(result, merged)
|
if !includeMeans {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyHistograms(result *model.MonitorVLLM, histograms map[string]histogram) {
|
|
||||||
for _, item := range []struct {
|
for _, item := range []struct {
|
||||||
name string
|
name string
|
||||||
mean **float64
|
mean **float64
|
||||||
@@ -185,6 +181,8 @@ func applyHistograms(result *model.MonitorVLLM, histograms map[string]histogram)
|
|||||||
{"e2e_request_latency_seconds", &result.RequestLatency, []**float64{&result.RequestLatencyP50, &result.RequestLatencyP90, &result.RequestLatencyP95, &result.RequestLatencyP99}},
|
{"e2e_request_latency_seconds", &result.RequestLatency, []**float64{&result.RequestLatencyP50, &result.RequestLatencyP90, &result.RequestLatencyP95, &result.RequestLatencyP99}},
|
||||||
{"request_prefill_time_seconds", &result.PrefillTime, nil},
|
{"request_prefill_time_seconds", &result.PrefillTime, nil},
|
||||||
{"request_decode_time_seconds", &result.DecodeTime, nil},
|
{"request_decode_time_seconds", &result.DecodeTime, nil},
|
||||||
|
{"request_queue_time_seconds", &result.QueueTime, nil},
|
||||||
|
{"request_generation_tokens", &result.AverageGenerationTokens, nil},
|
||||||
} {
|
} {
|
||||||
histogram, ok := histograms[item.name]
|
histogram, ok := histograms[item.name]
|
||||||
if !ok || histogram.Count <= 0 {
|
if !ok || histogram.Count <= 0 {
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package vllm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||||
|
)
|
||||||
|
|
||||||
|
type requestMetrics struct {
|
||||||
|
Seconds float64 `json:"seconds"`
|
||||||
|
PrefixHits *float64 `json:"prefixHits,omitempty"`
|
||||||
|
PrefixQueries *float64 `json:"prefixQueries,omitempty"`
|
||||||
|
FinishReasons map[string]float64 `json:"finishReasons,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateRequestMetrics(current, previous Metrics, elapsed float64, result *model.MonitorVLLM) error {
|
||||||
|
sample := requestMetrics{Seconds: elapsed, FinishReasons: map[string]float64{}}
|
||||||
|
hits := counterDelta(current["prefix_cache_hits_total"], previous["prefix_cache_hits_total"])
|
||||||
|
queries := counterDelta(current["prefix_cache_queries_total"], previous["prefix_cache_queries_total"])
|
||||||
|
if hits != nil && queries != nil && *hits <= *queries {
|
||||||
|
sample.PrefixHits, sample.PrefixQueries = hits, queries
|
||||||
|
if *queries > 0 {
|
||||||
|
rate := *hits / *queries * 100
|
||||||
|
result.PrefixCacheHitRate = &rate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
before := groupFinishReasons(previous["request_success_total"])
|
||||||
|
for reason, values := range groupFinishReasons(current["request_success_total"]) {
|
||||||
|
if delta := counterDelta(values, before[reason]); delta != nil {
|
||||||
|
sample.FinishReasons[reason] = *delta
|
||||||
|
if result.FinishReasons == nil {
|
||||||
|
result.FinishReasons = map[string]float64{}
|
||||||
|
}
|
||||||
|
result.FinishReasons[reason] = *delta / elapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(sample)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result.RequestDeltas = string(encoded)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupFinishReasons(samples map[string]float64) map[string]map[string]float64 {
|
||||||
|
groups := map[string]map[string]float64{}
|
||||||
|
for labels, value := range samples {
|
||||||
|
for _, label := range re.GetRegex(re.VLLMMetricLabelPattern).FindAllStringSubmatch(labels, -1) {
|
||||||
|
if label[1] != "finished_reason" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reason, err := strconv.Unquote(label[2])
|
||||||
|
if err != nil || reason == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if groups[reason] == nil {
|
||||||
|
groups[reason] = map[string]float64{}
|
||||||
|
}
|
||||||
|
groups[reason][labels] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
@@ -139,33 +139,21 @@ func TestCustomWebhook(
|
|||||||
resolved dto.AlertCustomWebhookResolvedConfig,
|
resolved dto.AlertCustomWebhookResolvedConfig,
|
||||||
transport *http.Transport,
|
transport *http.Transport,
|
||||||
agentInfo *dto.AgentInfo,
|
agentInfo *dto.AgentInfo,
|
||||||
|
lang string,
|
||||||
) (dto.AlertConfigTestResult, error) {
|
) (dto.AlertConfigTestResult, error) {
|
||||||
request, err := buildResolvedCustomWebhookRequest(resolved, webhook_sender.TemplateData{
|
request, err := buildResolvedCustomWebhookRequest(resolved, webhook_sender.TemplateData{
|
||||||
Title: "1Panel Webhook Test",
|
Title: i18n.GetMsgByKeyAndLang(lang, "TestAlertTitle"),
|
||||||
Message: "This is a test notification from 1Panel.",
|
Message: i18n.GetMsgByKeyAndLang(lang, "TestAlertMessage"),
|
||||||
Type: "test",
|
Type: "test",
|
||||||
NodeName: customWebhookNodeName(agentInfo),
|
NodeName: customWebhookNodeName(agentInfo),
|
||||||
Timestamp: customWebhookNow(),
|
Timestamp: customWebhookNow(),
|
||||||
}, transport)
|
}, transport)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dto.AlertConfigTestResult{}, err
|
return CustomWebhookTestResult(webhook_sender.Request{}, webhook_sender.Result{}, err), nil
|
||||||
}
|
}
|
||||||
request.CaptureResponse = true
|
request.CaptureResponse = true
|
||||||
result, executeErr := webhook_sender.Execute(context.Background(), request)
|
result, executeErr := webhook_sender.Execute(context.Background(), request)
|
||||||
durationMillis := result.Duration.Milliseconds()
|
return CustomWebhookTestResult(request, result, executeErr), nil
|
||||||
if result.Duration > 0 && durationMillis == 0 {
|
|
||||||
durationMillis = 1
|
|
||||||
}
|
|
||||||
testResult := dto.AlertConfigTestResult{
|
|
||||||
Success: executeErr == nil,
|
|
||||||
StatusCode: result.StatusCode,
|
|
||||||
Duration: durationMillis,
|
|
||||||
Response: result.Response,
|
|
||||||
}
|
|
||||||
if executeErr != nil {
|
|
||||||
testResult.Message = executeErr.Error()
|
|
||||||
}
|
|
||||||
return testResult, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildResolvedCustomWebhookRequest(
|
func buildResolvedCustomWebhookRequest(
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package alert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/webhook_sender"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CustomWebhookTestResult(request webhook_sender.Request, result webhook_sender.Result, executeErr error) dto.AlertConfigTestResult {
|
||||||
|
durationMillis := result.Duration.Milliseconds()
|
||||||
|
if result.Duration > 0 && durationMillis == 0 {
|
||||||
|
durationMillis = 1
|
||||||
|
}
|
||||||
|
testResult := dto.AlertConfigTestResult{
|
||||||
|
Success: executeErr == nil,
|
||||||
|
StatusCode: result.StatusCode,
|
||||||
|
Duration: durationMillis,
|
||||||
|
}
|
||||||
|
if executeErr == nil {
|
||||||
|
return testResult
|
||||||
|
}
|
||||||
|
if global.LOG != nil {
|
||||||
|
global.LOG.Warnf("webhook test failed: statusCode=%d durationMs=%d error=%q response=%q",
|
||||||
|
result.StatusCode, durationMillis,
|
||||||
|
webhook_sender.DiagnosticText(executeErr.Error(), request),
|
||||||
|
webhook_sender.DiagnosticText(result.Response, request))
|
||||||
|
}
|
||||||
|
return testResult
|
||||||
|
}
|
||||||
@@ -251,6 +251,10 @@ func (c *CommandHelper) buildPipeCommands(ctx context.Context, commands []PipeCo
|
|||||||
cmdItem.SysProcAttr = &syscall.SysProcAttr{
|
cmdItem.SysProcAttr = &syscall.SysProcAttr{
|
||||||
Setpgid: true,
|
Setpgid: true,
|
||||||
}
|
}
|
||||||
|
cmdItem.Cancel = func() error {
|
||||||
|
_ = syscall.Kill(-cmdItem.Process.Pid, syscall.SIGKILL)
|
||||||
|
return cmdItem.Process.Kill()
|
||||||
|
}
|
||||||
cmds = append(cmds, cmdItem)
|
cmds = append(cmds, cmdItem)
|
||||||
}
|
}
|
||||||
return cmds
|
return cmds
|
||||||
@@ -335,6 +339,12 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
|
|||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
Setpgid: true,
|
Setpgid: true,
|
||||||
}
|
}
|
||||||
|
if newContext != nil {
|
||||||
|
cmd.Cancel = func() error {
|
||||||
|
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||||
|
return cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
customWriter := &CustomWriter{taskItem: c.taskItem}
|
customWriter := &CustomWriter{taskItem: c.taskItem}
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
@@ -398,6 +408,9 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
|
|||||||
}()
|
}()
|
||||||
select {
|
select {
|
||||||
case err := <-done:
|
case err := <-done:
|
||||||
|
if newContext != nil && newContext.Err() != nil {
|
||||||
|
return "", c.pipeResultErr(newContext, err)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return handleErr(&stdout, &stderr, c.IgnoreExist1, err)
|
return handleErr(&stdout, &stderr, c.IgnoreExist1, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,43 +7,20 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadDBConnByPath(fullPath, dbName string) *gorm.DB {
|
func LoadDBConnByPath(fullPath, dbName string) *gorm.DB {
|
||||||
if _, err := createDirWhenNotExist(true, path.Dir(fullPath)); err != nil {
|
|
||||||
panic(fmt.Errorf("init db dir failed, err: %v", err))
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(fullPath); err != nil {
|
|
||||||
f, err := os.Create(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("init %s db file failed, err: %v", dbName, err))
|
|
||||||
}
|
|
||||||
_ = f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := GetDBWithPath(fullPath)
|
db, err := GetDBWithPath(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(fmt.Errorf("init %s db failed, err: %w", dbName, err))
|
||||||
}
|
}
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadDBConnByPathWithErr(fullPath, dbName string) (*gorm.DB, error) {
|
func LoadDBConnByPathWithErr(fullPath, dbName string) (*gorm.DB, error) {
|
||||||
if _, err := createDirWhenNotExist(true, path.Dir(fullPath)); err != nil {
|
|
||||||
return nil, fmt.Errorf("init db dir failed, err: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(fullPath); err != nil {
|
|
||||||
f, err := os.Create(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("init %s db file failed, err: %v", dbName, err)
|
|
||||||
}
|
|
||||||
_ = f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := GetDBWithPath(fullPath)
|
db, err := GetDBWithPath(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("init %s db failed, err: %v", dbName, err)
|
return nil, fmt.Errorf("init %s db failed, err: %v", dbName, err)
|
||||||
@@ -60,6 +37,30 @@ func CloseDB(db *gorm.DB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetDBWithPath(dbPath string) (*gorm.DB, error) {
|
func GetDBWithPath(dbPath string) (*gorm.DB, error) {
|
||||||
|
dbDir := path.Dir(dbPath)
|
||||||
|
if err := os.MkdirAll(dbDir, 0700); err != nil {
|
||||||
|
return nil, fmt.Errorf("init db dir failed, err: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.Chmod(dbDir, 0700); err != nil {
|
||||||
|
return nil, fmt.Errorf("set db dir permissions failed, err: %w", err)
|
||||||
|
}
|
||||||
|
file, err := os.OpenFile(dbPath, os.O_CREATE|os.O_RDWR, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init db file failed, err: %w", err)
|
||||||
|
}
|
||||||
|
if err := file.Chmod(0600); err != nil {
|
||||||
|
_ = file.Close()
|
||||||
|
return nil, fmt.Errorf("set db file permissions failed, err: %w", err)
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, suffix := range []string{"-wal", "-shm", "-journal"} {
|
||||||
|
if err := os.Chmod(dbPath+suffix, 0600); err != nil && !os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("set db sidecar permissions failed, err: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
Logger: newLogger(),
|
Logger: newLogger(),
|
||||||
@@ -104,17 +105,3 @@ func newLogger() logger.Interface {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDirWhenNotExist(isDir bool, pathItem string) (string, error) {
|
|
||||||
checkPath := pathItem
|
|
||||||
if !isDir {
|
|
||||||
checkPath = path.Dir(pathItem)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(checkPath); err != nil && os.IsNotExist(err) {
|
|
||||||
if err = os.MkdirAll(checkPath, os.ModePerm); err != nil {
|
|
||||||
global.LOG.Errorf("mkdir %s failed, err: %v", checkPath, err)
|
|
||||||
return pathItem, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pathItem, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -158,6 +158,9 @@ func GetComposeImages(filePath string, projectName ...string) ([]string, error)
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return images, nil
|
return images, nil
|
||||||
}
|
}
|
||||||
|
if strings.Contains(filePath, ",") {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
content, readErr := os.ReadFile(filePath)
|
content, readErr := os.ReadFile(filePath)
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ func dockerPolicyRuleKey(tokens []string, backend string) string {
|
|||||||
if address, err := netip.ParseAddr(value); err == nil {
|
if address, err := netip.ParseAddr(value); err == nil {
|
||||||
value = netip.PrefixFrom(address, address.BitLen()).String()
|
value = netip.PrefixFrom(address, address.BitLen()).String()
|
||||||
} else if prefix, err := netip.ParsePrefix(value); err == nil {
|
} else if prefix, err := netip.ParsePrefix(value); err == nil {
|
||||||
|
if prefix.Bits() == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
value = prefix.Masked().String()
|
value = prefix.Masked().String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +159,13 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
|
|||||||
group.acceptedSource = append(group.acceptedSource, source)
|
group.acceptedSource = append(group.acceptedSource, source)
|
||||||
case action == "accept":
|
case action == "accept":
|
||||||
group.acceptAll = true
|
group.acceptAll = true
|
||||||
case action == "return" && source != "":
|
case action == "return":
|
||||||
|
if source == "" {
|
||||||
|
source = "0.0.0.0/0"
|
||||||
|
if family == FamilyIPv6 {
|
||||||
|
source = "::/0"
|
||||||
|
}
|
||||||
|
}
|
||||||
group.allowedSource = append(group.allowedSource, source)
|
group.allowedSource = append(group.allowedSource, source)
|
||||||
case action == "drop" && source != "":
|
case action == "drop" && source != "":
|
||||||
group.droppedSource = append(group.droppedSource, source)
|
group.droppedSource = append(group.droppedSource, source)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func (a *Adapter) CheckRule(ctx context.Context, rule filter.FirewallRule) error
|
|||||||
if rule.Priority == nil || *rule.Priority == 0 {
|
if rule.Priority == nil || *rule.Priority == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
supported, err := a.supportsRichRulePriority(ctx)
|
supported, err := a.SupportsRichRulePriority(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ func (a *Adapter) CheckRule(ctx context.Context, rule filter.FirewallRule) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Adapter) supportsRichRulePriority(ctx context.Context) (bool, error) {
|
func (a *Adapter) SupportsRichRulePriority(ctx context.Context) (bool, error) {
|
||||||
a.prioritySupportMu.Lock()
|
a.prioritySupportMu.Lock()
|
||||||
defer a.prioritySupportMu.Unlock()
|
defer a.prioritySupportMu.Unlock()
|
||||||
if a.richRulePrioritySupported != nil {
|
if a.richRulePrioritySupported != nil {
|
||||||
@@ -82,6 +82,52 @@ func (a *Adapter) supportsRichRulePriority(ctx context.Context) (bool, error) {
|
|||||||
return supported, nil
|
return supported, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RemoveRichRulePriority(raw string) (string, error) {
|
||||||
|
if strings.ContainsAny(raw, "\r\n\x00") {
|
||||||
|
return "", filter.ErrInvalidRule
|
||||||
|
}
|
||||||
|
parts := make([]string, 0)
|
||||||
|
removed := false
|
||||||
|
for start := 0; start < len(raw); {
|
||||||
|
if raw[start] == ' ' || raw[start] == '\t' {
|
||||||
|
start++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
end, quote := start, byte(0)
|
||||||
|
for end < len(raw) {
|
||||||
|
char := raw[end]
|
||||||
|
if char == '\\' && quote != '\'' {
|
||||||
|
end += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if quote != 0 {
|
||||||
|
if char == quote {
|
||||||
|
quote = 0
|
||||||
|
}
|
||||||
|
} else if char == '\'' || char == '"' {
|
||||||
|
quote = char
|
||||||
|
} else if char == ' ' || char == '\t' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
if quote != 0 || end > len(raw) {
|
||||||
|
return "", filter.ErrInvalidRule
|
||||||
|
}
|
||||||
|
token := raw[start:end]
|
||||||
|
if strings.HasPrefix(token, "priority=") {
|
||||||
|
removed = true
|
||||||
|
} else {
|
||||||
|
parts = append(parts, token)
|
||||||
|
}
|
||||||
|
start = end
|
||||||
|
}
|
||||||
|
if !removed || len(parts) == 0 || parts[0] != "rule" {
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " "), nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseFirewalldVersion(output string) (int, int, error) {
|
func parseFirewalldVersion(output string) (int, int, error) {
|
||||||
version := strings.TrimSpace(output)
|
version := strings.TrimSpace(output)
|
||||||
parts := strings.Split(version, ".")
|
parts := strings.Split(version, ".")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package forwarding
|
package forwarding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -8,19 +9,27 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ensureFamilyForwardingSysctls(system forwardingSystem, family string) error {
|
func ensureFamilyForwardingSysctls(system forwardingSystem, family string) error {
|
||||||
withIPv6 := family == FamilyIPv6
|
withIPv6 := family == FamilyIPv6
|
||||||
if withIPv6 {
|
if withIPv6 {
|
||||||
interfaces, err := IPv6RAInterfaces(system.ReadFile)
|
interfaces, err := IPv6RAInterfaces(system.ReadFile, LoadDockerIPv4BridgePorts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("check IPv6 Router Advertisement: %w", err)
|
global.LOG.Warnf("IPv6 forwarding blocked: failed to check Router Advertisement: %v", err)
|
||||||
|
return buserr.New("ErrIPv6RACheckFailed")
|
||||||
}
|
}
|
||||||
if len(interfaces) > 0 {
|
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, ", "))
|
global.LOG.Warnf("IPv6 forwarding blocked: interfaces %s may depend on RA/SLAAC with accept_ra=1; review the network configuration and persist accept_ra=2 on interfaces that require RA before retrying", strings.Join(interfaces, ", "))
|
||||||
|
return buserr.New("ErrIPv6RARisk")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
paths := []string{"/proc/sys/net/ipv4/ip_forward"}
|
paths := []string{"/proc/sys/net/ipv4/ip_forward"}
|
||||||
@@ -43,7 +52,7 @@ func ensureFamilyForwardingSysctls(system forwardingSystem, family string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
|
func IPv6RAInterfaces(readFile func(string) ([]byte, error), loadDockerPorts func() (map[string]bool, error)) ([]string, error) {
|
||||||
addresses, err := readFile("/proc/net/if_inet6")
|
addresses, err := readFile("/proc/net/if_inet6")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -67,21 +76,16 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, exists := globalAddresses[name]; !exists {
|
globalAddresses[name] = globalAddresses[name] || fields[3] == "00"
|
||||||
globalAddresses[name] = false
|
if fields[3] == "00" && flags&permanentAddress == 0 {
|
||||||
}
|
|
||||||
if fields[3] == "00" {
|
|
||||||
globalAddresses[name] = true
|
|
||||||
if flags&permanentAddress == 0 {
|
|
||||||
raInterfaces[name] = true
|
raInterfaces[name] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
routes, err := readFile("/proc/net/ipv6_route")
|
routes, err := readFile("/proc/net/ipv6_route")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
const raRouteFlags = 0x00010000 | 0x00800000
|
const raRouteFlags = 0x00010000 | 0x00080000 | 0x00800000
|
||||||
for _, line := range strings.Split(string(routes), "\n") {
|
for _, line := range strings.Split(string(routes), "\n") {
|
||||||
fields := strings.Fields(line)
|
fields := strings.Fields(line)
|
||||||
if len(fields) == 0 {
|
if len(fields) == 0 {
|
||||||
@@ -98,12 +102,15 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
|
|||||||
raInterfaces[fields[9]] = true
|
raInterfaces[fields[9]] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
uncertainInterfaces := make(map[string]bool)
|
||||||
for name, hasGlobalAddress := range globalAddresses {
|
for name, hasGlobalAddress := range globalAddresses {
|
||||||
if !hasGlobalAddress {
|
if !hasGlobalAddress && !raInterfaces[name] {
|
||||||
raInterfaces[name] = true
|
raInterfaces[name] = true
|
||||||
|
uncertainInterfaces[name] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var interfaces []string
|
var interfaces []string
|
||||||
|
checkDockerPorts := false
|
||||||
for name := range raInterfaces {
|
for name := range raInterfaces {
|
||||||
if name == "lo" {
|
if name == "lo" {
|
||||||
continue
|
continue
|
||||||
@@ -118,15 +125,119 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
|
|||||||
switch strings.TrimSpace(string(value)) {
|
switch strings.TrimSpace(string(value)) {
|
||||||
case "1":
|
case "1":
|
||||||
interfaces = append(interfaces, name)
|
interfaces = append(interfaces, name)
|
||||||
|
checkDockerPorts = checkDockerPorts || uncertainInterfaces[name]
|
||||||
case "0", "2":
|
case "0", "2":
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid accept_ra value for %s", name)
|
return nil, fmt.Errorf("invalid accept_ra value for %s", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if checkDockerPorts {
|
||||||
|
ports, err := loadDockerPorts()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Warnf("IPv6 forwarding RA check: cannot exclude Docker bridge ports: %v", err)
|
||||||
|
} else {
|
||||||
|
kept := interfaces[:0]
|
||||||
|
for _, name := range interfaces {
|
||||||
|
if !uncertainInterfaces[name] || !ports[name] {
|
||||||
|
kept = append(kept, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interfaces = kept
|
||||||
|
}
|
||||||
|
}
|
||||||
sort.Strings(interfaces)
|
sort.Strings(interfaces)
|
||||||
return interfaces, nil
|
return interfaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LoadDockerIPv4BridgePorts() (map[string]bool, error) {
|
||||||
|
cli, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cli.Close()
|
||||||
|
if !strings.HasPrefix(cli.DaemonHost(), "unix://") {
|
||||||
|
return nil, errors.New("cannot verify local bridge ports using a remote Docker daemon")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
info, err := cli.Info(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, option := range info.SecurityOptions {
|
||||||
|
if option == "name=rootless" {
|
||||||
|
return nil, errors.New("cannot verify host bridge ports using a rootless Docker daemon")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
networks, err := cli.NetworkList(ctx, network.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
command := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(5*time.Second))
|
||||||
|
bridges, err := command.RunWithOptionalSudoAndStdout("ip", "-o", "link", "show", "type", "bridge")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ports, err := command.RunWithOptionalSudoAndStdout("ip", "-o", "link", "show", "type", "veth")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dockerIPv4BridgePorts(networks, bridges, ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dockerIPv4BridgePorts(networks []network.Summary, bridgeOutput, portOutput string) (map[string]bool, error) {
|
||||||
|
bridges := make(map[string]bool)
|
||||||
|
for _, item := range networks {
|
||||||
|
if item.Driver != "bridge" || item.EnableIPv6 || item.ConfigOnly {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := item.Options["com.docker.network.bridge.name"]
|
||||||
|
if name == "" {
|
||||||
|
if item.Options["com.docker.network.bridge.default_bridge"] == "true" {
|
||||||
|
name = "docker0"
|
||||||
|
} else if len(item.ID) >= 12 {
|
||||||
|
name = "br-" + item.ID[:12]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name != "" {
|
||||||
|
bridges[name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
confirmedBridges := make(map[string]bool)
|
||||||
|
ports := make(map[string]bool)
|
||||||
|
for index, output := range []string{bridgeOutput, portOutput} {
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
header, _, _ := strings.Cut(line, "\\")
|
||||||
|
fields := strings.Fields(header)
|
||||||
|
if len(fields) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(fields) < 3 || !strings.HasSuffix(fields[0], ":") || !strings.HasSuffix(fields[1], ":") {
|
||||||
|
return nil, fmt.Errorf("invalid network link entry: %q", line)
|
||||||
|
}
|
||||||
|
if _, err := strconv.Atoi(strings.TrimSuffix(fields[0], ":")); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid network link entry: %q", line)
|
||||||
|
}
|
||||||
|
name, _, _ := strings.Cut(strings.TrimSuffix(fields[1], ":"), "@")
|
||||||
|
if index == 0 {
|
||||||
|
if bridges[name] {
|
||||||
|
confirmedBridges[name] = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := 3; i+1 < len(fields); i++ {
|
||||||
|
if fields[i] == "master" {
|
||||||
|
if confirmedBridges[fields[i+1]] {
|
||||||
|
ports[name] = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ports, nil
|
||||||
|
}
|
||||||
|
|
||||||
func enableFamilyForwardingSysctls(content, family string) string {
|
func enableFamilyForwardingSysctls(content, family string) string {
|
||||||
lines := strings.Split(strings.TrimRight(content, "\n"), "\n")
|
lines := strings.Split(strings.TrimRight(content, "\n"), "\n")
|
||||||
wanted := map[string]string{"net.ipv4.ip_forward": "net.ipv4.ip_forward = 1"}
|
wanted := map[string]string{"net.ipv4.ip_forward": "net.ipv4.ip_forward = 1"}
|
||||||
|
|||||||
@@ -209,18 +209,36 @@ func buildBaseChainBindingsRestoreScript(output string, bind bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func baseChainBindingCommands(output string, bind bool) []string {
|
func baseChainBindingCommands(output string, bind bool) []string {
|
||||||
lines := make([]string, 0, len(BasicChains())*2)
|
chains := BasicChains()
|
||||||
for _, chain := range BasicChains() {
|
lines := make([]string, 0, len(chains)*2)
|
||||||
binding := "-A " + InputChain + " -j " + chain
|
position, insertAt := 0, 1
|
||||||
for _, current := range strings.Split(output, "\n") {
|
for _, current := range strings.Split(output, "\n") {
|
||||||
if strings.TrimSpace(current) == binding {
|
current = strings.TrimSpace(current)
|
||||||
|
if !strings.HasPrefix(current, "-A "+InputChain+" ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if chain := strings.TrimPrefix(current, "-A "+InputChain+" -j "); slices.Contains(chains, chain) {
|
||||||
lines = append(lines, "-D "+InputChain+" -j "+chain)
|
lines = append(lines, "-D "+InputChain+" -j "+chain)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
position++
|
||||||
|
if !bind {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields, err := shellwords.Parse(current)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for index := 2; index+1 < len(fields); index++ {
|
||||||
|
if fields[index] == "-j" && strings.HasPrefix(fields[index+1], "f2b-") {
|
||||||
|
insertAt = position + 1
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if bind {
|
if bind {
|
||||||
for index, chain := range BasicChains() {
|
for index, chain := range chains {
|
||||||
lines = append(lines, fmt.Sprintf("-I %s %d -j %s", InputChain, index+1, chain))
|
lines = append(lines, fmt.Sprintf("-I %s %d -j %s", InputChain, insertAt+index, chain))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/go-acme/lego/v5/providers/dns/namecheap"
|
"github.com/go-acme/lego/v5/providers/dns/namecheap"
|
||||||
"github.com/go-acme/lego/v5/providers/dns/namedotcom"
|
"github.com/go-acme/lego/v5/providers/dns/namedotcom"
|
||||||
"github.com/go-acme/lego/v5/providers/dns/namesilo"
|
"github.com/go-acme/lego/v5/providers/dns/namesilo"
|
||||||
|
"github.com/go-acme/lego/v5/providers/dns/netcup"
|
||||||
"github.com/go-acme/lego/v5/providers/dns/ovh"
|
"github.com/go-acme/lego/v5/providers/dns/ovh"
|
||||||
"github.com/go-acme/lego/v5/providers/dns/porkbun"
|
"github.com/go-acme/lego/v5/providers/dns/porkbun"
|
||||||
"github.com/go-acme/lego/v5/providers/dns/rainyun"
|
"github.com/go-acme/lego/v5/providers/dns/rainyun"
|
||||||
@@ -58,6 +59,7 @@ const (
|
|||||||
NameCheap DnsType = "NameCheap"
|
NameCheap DnsType = "NameCheap"
|
||||||
NameCom DnsType = "NameCom"
|
NameCom DnsType = "NameCom"
|
||||||
NameSilo DnsType = "NameSilo"
|
NameSilo DnsType = "NameSilo"
|
||||||
|
Netcup DnsType = "Netcup"
|
||||||
Ovh DnsType = "Ovh"
|
Ovh DnsType = "Ovh"
|
||||||
PorkBun DnsType = "PorkBun"
|
PorkBun DnsType = "PorkBun"
|
||||||
RainYun DnsType = "RainYun"
|
RainYun DnsType = "RainYun"
|
||||||
@@ -92,6 +94,7 @@ type DNSParam struct {
|
|||||||
Endpoint string `json:"endpoint"`
|
Endpoint string `json:"endpoint"`
|
||||||
AccessToken string `json:"accessToken"`
|
AccessToken string `json:"accessToken"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
|
CustomerNumber string `json:"customerNumber"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -164,6 +167,14 @@ func getDNSProviderConfig(dnsType DnsType, params string, httpClient *http.Clien
|
|||||||
config.PropagationTimeout = propagationTimeout
|
config.PropagationTimeout = propagationTimeout
|
||||||
config.PollingInterval = pollingInterval
|
config.PollingInterval = pollingInterval
|
||||||
p, err = namesilo.NewDNSProviderConfig(config)
|
p, err = namesilo.NewDNSProviderConfig(config)
|
||||||
|
case Netcup:
|
||||||
|
config := newDNSProviderConfig(netcup.NewDefaultConfig(), httpClient)
|
||||||
|
config.Customer = param.CustomerNumber
|
||||||
|
config.Key = param.APIkey
|
||||||
|
config.Password = param.Password
|
||||||
|
config.PropagationTimeout = propagationTimeout
|
||||||
|
config.PollingInterval = pollingInterval
|
||||||
|
p, err = netcup.NewDNSProviderConfig(config)
|
||||||
case Godaddy:
|
case Godaddy:
|
||||||
config := newDNSProviderConfig(godaddy.NewDefaultConfig(), httpClient)
|
config := newDNSProviderConfig(godaddy.NewDefaultConfig(), httpClient)
|
||||||
config.APIKey = param.APIkey
|
config.APIKey = param.APIkey
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func Execute(ctx context.Context, input Request) (Result, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := validateResponse(input.Preset, resp.StatusCode, responseBody); err != nil {
|
if err := validateResponse(input.Preset, resp.StatusCode, responseBody); err != nil {
|
||||||
return result, err
|
return result, safeResponseError(err, input, target)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -332,6 +332,14 @@ func (b *proxyConnectionBody) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func captureResponse(body []byte, input Request, target preparedTarget) string {
|
func captureResponse(body []byte, input Request, target preparedTarget) string {
|
||||||
|
var document any
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(body))
|
||||||
|
decoder.UseNumber()
|
||||||
|
if json.Valid(body) && decoder.Decode(&document) == nil {
|
||||||
|
if canonical, err := json.Marshal(document); err == nil {
|
||||||
|
body = canonical
|
||||||
|
}
|
||||||
|
}
|
||||||
response := strings.ToValidUTF8(string(body), "\uFFFD")
|
response := strings.ToValidUTF8(string(body), "\uFFFD")
|
||||||
redactions := make([]string, 0, len(input.Headers)+8)
|
redactions := make([]string, 0, len(input.Headers)+8)
|
||||||
redactions = append(redactions, urlRedactionValues(input.URL)...)
|
redactions = append(redactions, urlRedactionValues(input.URL)...)
|
||||||
@@ -345,17 +353,105 @@ func captureResponse(body []byte, input Request, target preparedTarget) string {
|
|||||||
}
|
}
|
||||||
redactions = append(redactions, headerRedactionValues(name, value)...)
|
redactions = append(redactions, headerRedactionValues(name, value)...)
|
||||||
}
|
}
|
||||||
|
redactions = append(redactions, bodyRedactionValues(input)...)
|
||||||
|
for _, value := range append([]string(nil), redactions...) {
|
||||||
|
if encoded, err := json.Marshal(value); err == nil && len(encoded) > 2 {
|
||||||
|
redactions = append(redactions, string(encoded[1:len(encoded)-1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
sort.SliceStable(redactions, func(i, j int) bool {
|
sort.SliceStable(redactions, func(i, j int) bool {
|
||||||
return len(redactions[i]) > len(redactions[j])
|
return len(redactions[i]) > len(redactions[j])
|
||||||
})
|
})
|
||||||
|
seen := make(map[string]struct{}, len(redactions))
|
||||||
|
replacements := make([]string, 0, len(redactions)*2)
|
||||||
for _, value := range redactions {
|
for _, value := range redactions {
|
||||||
if value != "" {
|
if value == "" {
|
||||||
response = strings.ReplaceAll(response, value, "[REDACTED]")
|
continue
|
||||||
}
|
}
|
||||||
|
if _, exists := seen[value]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
replacements = append(replacements, value, "[REDACTED]")
|
||||||
|
}
|
||||||
|
if len(replacements) > 0 {
|
||||||
|
response = strings.NewReplacer(replacements...).Replace(response)
|
||||||
}
|
}
|
||||||
return truncateUTF8(response, MaxCapturedResponseBytes)
|
return truncateUTF8(response, MaxCapturedResponseBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bodyRedactionValues(input Request) []string {
|
||||||
|
values := []string{string(input.Body)}
|
||||||
|
switch input.Format {
|
||||||
|
case BodyJSON:
|
||||||
|
var document any
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(input.Body))
|
||||||
|
decoder.UseNumber()
|
||||||
|
if decoder.Decode(&document) == nil {
|
||||||
|
var collect func(any, string)
|
||||||
|
collect = func(value any, key string) {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case string:
|
||||||
|
if len(typed) >= 4 || typed != "" && sensitiveBodyKey(key) {
|
||||||
|
values = append(values, typed)
|
||||||
|
}
|
||||||
|
case json.Number:
|
||||||
|
if sensitiveBodyKey(key) {
|
||||||
|
values = append(values, typed.String())
|
||||||
|
}
|
||||||
|
case map[string]any:
|
||||||
|
for childKey, child := range typed {
|
||||||
|
collect(child, childKey)
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, child := range typed {
|
||||||
|
collect(child, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collect(document, "")
|
||||||
|
}
|
||||||
|
case BodyForm:
|
||||||
|
if fields, err := url.ParseQuery(string(input.Body)); err == nil {
|
||||||
|
for key, fieldValues := range fields {
|
||||||
|
for _, value := range fieldValues {
|
||||||
|
if len(value) >= 4 || value != "" && sensitiveBodyKey(key) {
|
||||||
|
values = append(values, value)
|
||||||
|
encoded := url.QueryEscape(value)
|
||||||
|
percentSpaces := strings.ReplaceAll(encoded, "+", "%20")
|
||||||
|
values = append(values, encoded, percentSpaces, lowercasePercentEscapes(encoded), lowercasePercentEscapes(percentSpaces))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func lowercasePercentEscapes(value string) string {
|
||||||
|
encoded := []byte(value)
|
||||||
|
for i := 0; i+2 < len(encoded); i++ {
|
||||||
|
if encoded[i] != '%' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for j := i + 1; j <= i+2; j++ {
|
||||||
|
if encoded[j] >= 'A' && encoded[j] <= 'F' {
|
||||||
|
encoded[j] += 'a' - 'A'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
return string(encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sensitiveBodyKey(key string) bool {
|
||||||
|
normalized := strings.ToLower(strings.NewReplacer("-", "", "_", "").Replace(key))
|
||||||
|
return normalized == "key" || normalized == "authorization" || normalized == "cookie" ||
|
||||||
|
strings.Contains(normalized, "password") || strings.Contains(normalized, "passwd") ||
|
||||||
|
strings.Contains(normalized, "token") || strings.Contains(normalized, "secret") ||
|
||||||
|
strings.Contains(normalized, "credential") || strings.Contains(normalized, "apikey")
|
||||||
|
}
|
||||||
|
|
||||||
func urlRedactionValues(rawURL string) []string {
|
func urlRedactionValues(rawURL string) []string {
|
||||||
trimmed := strings.TrimSpace(rawURL)
|
trimmed := strings.TrimSpace(rawURL)
|
||||||
values := []string{trimmed}
|
values := []string{trimmed}
|
||||||
@@ -745,23 +841,3 @@ func isPinnedWebhookAddress(host string, addresses []net.IP) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateResponse(preset Preset, statusCode int, body []byte) error {
|
|
||||||
if preset == "" {
|
|
||||||
preset = PresetGeneric
|
|
||||||
}
|
|
||||||
switch preset {
|
|
||||||
case PresetSlack:
|
|
||||||
if statusCode != http.StatusOK || strings.TrimSpace(string(body)) != "ok" {
|
|
||||||
return errors.New("webhook provider rejected response")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case PresetGeneric, PresetDiscord, PresetTeams:
|
|
||||||
if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices {
|
|
||||||
return errors.New("webhook provider rejected response")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return errors.New("unsupported webhook preset")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package webhook_sender
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type responseRejection struct {
|
||||||
|
statusCode int
|
||||||
|
reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
func DiagnosticText(value string, input Request) string {
|
||||||
|
return captureResponse([]byte(value), input, preparedTarget{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *responseRejection) Error() string {
|
||||||
|
return fmt.Sprintf("webhook provider rejected response (HTTP %d)", e.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateResponse(preset Preset, statusCode int, body []byte) error {
|
||||||
|
if preset == "" {
|
||||||
|
preset = PresetGeneric
|
||||||
|
}
|
||||||
|
switch preset {
|
||||||
|
case PresetGeneric, PresetDiscord, PresetTeams, PresetSlack:
|
||||||
|
default:
|
||||||
|
return errors.New("unsupported webhook preset")
|
||||||
|
}
|
||||||
|
|
||||||
|
reason, failed := businessResponseFailure(body)
|
||||||
|
if preset == PresetSlack && reason == "" {
|
||||||
|
switch value := strings.TrimSpace(string(body)); value {
|
||||||
|
case "invalid_payload", "no_text", "invalid_token", "no_service", "no_service_id",
|
||||||
|
"no_active_hooks", "channel_not_found", "channel_is_archived",
|
||||||
|
"action_prohibited", "posting_to_general_channel_denied",
|
||||||
|
"team_disabled", "user_not_found", "too_many_attachments", "rollup_error":
|
||||||
|
reason = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices {
|
||||||
|
return &responseRejection{statusCode: statusCode, reason: reason}
|
||||||
|
}
|
||||||
|
if preset == PresetSlack {
|
||||||
|
if statusCode != http.StatusOK || strings.TrimSpace(string(body)) != "ok" {
|
||||||
|
return &responseRejection{statusCode: statusCode, reason: reason}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if failed {
|
||||||
|
return &responseRejection{statusCode: statusCode, reason: reason}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func businessResponseFailure(body []byte) (string, bool) {
|
||||||
|
var response map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(body, &response); err != nil || response == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
message := responseMessage(response)
|
||||||
|
if message == "" {
|
||||||
|
var errorText string
|
||||||
|
if json.Unmarshal(response["error"], &errorText) == nil {
|
||||||
|
message = strings.TrimSpace(errorText)
|
||||||
|
} else {
|
||||||
|
var nested map[string]json.RawMessage
|
||||||
|
if json.Unmarshal(response["error"], &nested) == nil {
|
||||||
|
message = responseMessage(nested)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, key := range []string{"errcode", "errCode", "error_code", "ErrorCode"} {
|
||||||
|
if raw, exists := response[key]; exists {
|
||||||
|
code, valid := responseCode(raw)
|
||||||
|
if !valid || !isSuccessResponseCode(code, false) {
|
||||||
|
return responseFailureReason(key, code, message), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, key := range []string{"success", "ok"} {
|
||||||
|
var success bool
|
||||||
|
if raw, exists := response[key]; exists && string(raw) != "null" && json.Unmarshal(raw, &success) == nil && !success {
|
||||||
|
return responseFailureReason(key, "false", message), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if raw, exists := response["error"]; exists {
|
||||||
|
var value any
|
||||||
|
if json.Unmarshal(raw, &value) == nil {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case string:
|
||||||
|
if strings.TrimSpace(typed) != "" {
|
||||||
|
return responseFailureReason("error", "", typed), true
|
||||||
|
}
|
||||||
|
case map[string]any:
|
||||||
|
if len(typed) != 0 {
|
||||||
|
return responseFailureReason("error", "", message), true
|
||||||
|
}
|
||||||
|
case bool:
|
||||||
|
if typed {
|
||||||
|
return responseFailureReason("error", "true", message), true
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
if typed != 0 {
|
||||||
|
return responseFailureReason("error", strconv.FormatFloat(typed, 'g', -1, 64), message), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if message != "" {
|
||||||
|
for _, key := range []string{"code", "Code", "StatusCode", "statusCode"} {
|
||||||
|
if raw, exists := response[key]; exists {
|
||||||
|
code, valid := responseCode(raw)
|
||||||
|
if !valid || !isSuccessResponseCode(code, true) {
|
||||||
|
return responseFailureReason(key, code, message), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseMessage(response map[string]json.RawMessage) string {
|
||||||
|
for _, key := range []string{"errmsg", "msg", "message", "description", "StatusMessage", "Message"} {
|
||||||
|
var message string
|
||||||
|
if json.Unmarshal(response[key], &message) == nil && strings.TrimSpace(message) != "" {
|
||||||
|
return strings.TrimSpace(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseCode(raw json.RawMessage) (string, bool) {
|
||||||
|
var code json.Number
|
||||||
|
if json.Unmarshal(raw, &code) == nil && code != "" {
|
||||||
|
return code.String(), true
|
||||||
|
}
|
||||||
|
var text string
|
||||||
|
if json.Unmarshal(raw, &text) == nil && strings.TrimSpace(text) != "" {
|
||||||
|
return strings.TrimSpace(text), true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSuccessResponseCode(code string, allowHTTP bool) bool {
|
||||||
|
number, err := strconv.ParseFloat(code, 64)
|
||||||
|
if err == nil {
|
||||||
|
return number == 0 || allowHTTP && number >= 200 && number < 300
|
||||||
|
}
|
||||||
|
return allowHTTP && (strings.EqualFold(code, "ok") || strings.EqualFold(code, "success"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseFailureReason(field, code, message string) string {
|
||||||
|
reason := field
|
||||||
|
if code != "" {
|
||||||
|
reason += "=" + code
|
||||||
|
}
|
||||||
|
if message != "" {
|
||||||
|
reason += ": " + message
|
||||||
|
}
|
||||||
|
return reason
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeResponseError(err error, input Request, target preparedTarget) error {
|
||||||
|
var rejection *responseRejection
|
||||||
|
if !input.CaptureResponse || !errors.As(err, &rejection) || rejection.reason == "" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reason := captureResponse([]byte(rejection.reason), input, target)
|
||||||
|
reason = strings.Join(strings.Fields(reason), " ")
|
||||||
|
if reason == "" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w: %s", rejection, truncateUTF8(reason, 512))
|
||||||
|
}
|
||||||
@@ -59,9 +59,9 @@ func (a *alertHelper) CreateTaskScanCustomWebhookAlertLog(alert dto.AlertDTO, al
|
|||||||
return providers.DeliveryResult{}, err
|
return providers.DeliveryResult{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *alertHelper) TestCustomWebhook(config dto.AlertCustomWebhookResolvedConfig) (dto.AlertConfigTestResult, error) {
|
func (a *alertHelper) TestCustomWebhook(config dto.AlertCustomWebhookResolvedConfig, lang string) (dto.AlertConfigTestResult, error) {
|
||||||
transport, agentInfo := loadCommunityCustomWebhookContext()
|
transport, agentInfo := loadCommunityCustomWebhookContext()
|
||||||
return alertUtil.TestCustomWebhook(config, transport, agentInfo)
|
return alertUtil.TestCustomWebhook(config, transport, agentInfo, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *alertHelper) GetLicenseErrorAlert() (uint, error) {
|
func (a *alertHelper) GetLicenseErrorAlert() (uint, error) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type AlertProvider interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CustomWebhookTester interface {
|
type CustomWebhookTester interface {
|
||||||
TestCustomWebhook(config dto.AlertCustomWebhookResolvedConfig) (dto.AlertConfigTestResult, error)
|
TestCustomWebhook(config dto.AlertCustomWebhookResolvedConfig, lang string) (dto.AlertConfigTestResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeliveryResult struct {
|
type DeliveryResult struct {
|
||||||
|
|||||||
@@ -495,7 +495,9 @@ func (u *UpgradeService) handleRollback(originalDir string, errStep int, svcInfo
|
|||||||
_ = settingRepo.Update("SystemStatus", "Free")
|
_ = settingRepo.Update("SystemStatus", "Free")
|
||||||
dbPath := path.Join(global.CONF.Base.InstallDir, "1panel")
|
dbPath := path.Join(global.CONF.Base.InstallDir, "1panel")
|
||||||
if _, err := os.Stat(path.Join(originalDir, "db")); err == nil {
|
if _, err := os.Stat(path.Join(originalDir, "db")); err == nil {
|
||||||
if err := files.CopyItem(true, true, path.Join(originalDir, "db"), dbPath); err != nil {
|
if err := os.Chmod(path.Join(originalDir, "db"), 0700); err != nil {
|
||||||
|
global.LOG.Errorf("set rollback db dir permissions failed, err: %v", err)
|
||||||
|
} else if err := files.CopyItem(true, true, path.Join(originalDir, "db"), dbPath); err != nil {
|
||||||
global.LOG.Errorf("rollback 1panel db failed, err: %v", err)
|
global.LOG.Errorf("rollback 1panel db failed, err: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ var restoreCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep4"))
|
fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep4"))
|
||||||
if _, err := os.Stat(path.Join(tmpPath, "db")); err == nil {
|
if _, err := os.Stat(path.Join(tmpPath, "db")); err == nil {
|
||||||
|
if err := os.Chmod(path.Join(tmpPath, "db"), 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
dbPath := path.Join(baseDir, "1panel")
|
dbPath := path.Join(baseDir, "1panel")
|
||||||
targetDBDir := path.Join(dbPath, "db")
|
targetDBDir := path.Join(dbPath, "db")
|
||||||
if err := os.RemoveAll(targetDBDir); err != nil {
|
if err := os.RemoveAll(targetDBDir); err != nil {
|
||||||
|
|||||||
@@ -15092,9 +15092,26 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"/databases/redis/install/cli": {
|
"/databases/redis/install/cli": {
|
||||||
"post": {
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "request",
|
||||||
|
"in": "body",
|
||||||
|
"name": "request",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.RedisCliInstall"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK"
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
@@ -18801,11 +18818,9 @@ const docTemplate = `{
|
|||||||
],
|
],
|
||||||
"x-panel-log": {
|
"x-panel-log": {
|
||||||
"BeforeFunctions": [],
|
"BeforeFunctions": [],
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"uuids"
|
"formatEN": "delete Docker port guard policies",
|
||||||
],
|
"formatZH": "删除 Docker 端口防护策略",
|
||||||
"formatEN": "delete Docker port guard policies [uuids]",
|
|
||||||
"formatZH": "删除 Docker 端口防护策略 [uuids]",
|
|
||||||
"paramKeys": []
|
"paramKeys": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19365,11 +19380,9 @@ const docTemplate = `{
|
|||||||
],
|
],
|
||||||
"x-panel-log": {
|
"x-panel-log": {
|
||||||
"BeforeFunctions": [],
|
"BeforeFunctions": [],
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"instanceKey"
|
"formatEN": "reorder firewall rule",
|
||||||
],
|
"formatZH": "调整防火墙规则顺序",
|
||||||
"formatEN": "reorder firewall rule [instanceKey]",
|
|
||||||
"formatZH": "调整防火墙规则顺序 [instanceKey]",
|
|
||||||
"paramKeys": []
|
"paramKeys": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19510,11 +19523,9 @@ const docTemplate = `{
|
|||||||
],
|
],
|
||||||
"x-panel-log": {
|
"x-panel-log": {
|
||||||
"BeforeFunctions": [],
|
"BeforeFunctions": [],
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"instanceKey"
|
"formatEN": "update firewall rule",
|
||||||
],
|
"formatZH": "更新防火墙规则",
|
||||||
"formatEN": "update firewall rule [instanceKey]",
|
|
||||||
"formatZH": "更新防火墙规则 [instanceKey]",
|
|
||||||
"paramKeys": []
|
"paramKeys": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36372,9 +36383,6 @@ const docTemplate = `{
|
|||||||
"bound": {
|
"bound": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"forwardPolicy": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"initialized": {
|
"initialized": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -40152,6 +40160,14 @@ const docTemplate = `{
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"dto.RedisCliInstall": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"taskID": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.RedisConf": {
|
"dto.RedisConf": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"containerName": {
|
"containerName": {
|
||||||
|
|||||||
@@ -15088,9 +15088,26 @@
|
|||||||
},
|
},
|
||||||
"/databases/redis/install/cli": {
|
"/databases/redis/install/cli": {
|
||||||
"post": {
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "request",
|
||||||
|
"in": "body",
|
||||||
|
"name": "request",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.RedisCliInstall"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK"
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
@@ -18797,11 +18814,9 @@
|
|||||||
],
|
],
|
||||||
"x-panel-log": {
|
"x-panel-log": {
|
||||||
"BeforeFunctions": [],
|
"BeforeFunctions": [],
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"uuids"
|
"formatEN": "delete Docker port guard policies",
|
||||||
],
|
"formatZH": "删除 Docker 端口防护策略",
|
||||||
"formatEN": "delete Docker port guard policies [uuids]",
|
|
||||||
"formatZH": "删除 Docker 端口防护策略 [uuids]",
|
|
||||||
"paramKeys": []
|
"paramKeys": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19361,11 +19376,9 @@
|
|||||||
],
|
],
|
||||||
"x-panel-log": {
|
"x-panel-log": {
|
||||||
"BeforeFunctions": [],
|
"BeforeFunctions": [],
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"instanceKey"
|
"formatEN": "reorder firewall rule",
|
||||||
],
|
"formatZH": "调整防火墙规则顺序",
|
||||||
"formatEN": "reorder firewall rule [instanceKey]",
|
|
||||||
"formatZH": "调整防火墙规则顺序 [instanceKey]",
|
|
||||||
"paramKeys": []
|
"paramKeys": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19506,11 +19519,9 @@
|
|||||||
],
|
],
|
||||||
"x-panel-log": {
|
"x-panel-log": {
|
||||||
"BeforeFunctions": [],
|
"BeforeFunctions": [],
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"instanceKey"
|
"formatEN": "update firewall rule",
|
||||||
],
|
"formatZH": "更新防火墙规则",
|
||||||
"formatEN": "update firewall rule [instanceKey]",
|
|
||||||
"formatZH": "更新防火墙规则 [instanceKey]",
|
|
||||||
"paramKeys": []
|
"paramKeys": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36368,9 +36379,6 @@
|
|||||||
"bound": {
|
"bound": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"forwardPolicy": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"initialized": {
|
"initialized": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -40148,6 +40156,14 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"dto.RedisCliInstall": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"taskID": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.RedisConf": {
|
"dto.RedisConf": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"containerName": {
|
"containerName": {
|
||||||
|
|||||||
+348
-352
@@ -265,8 +265,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "应用端口修改 [key]-[name] => [port]",
|
"formatZH": "应用端口修改 [key]-[name] =\u003e [port]",
|
||||||
"formatEN": "Application port update [key]-[name] => [port]"
|
"formatEN": "Application port update [key]-[name] =\u003e [port]"
|
||||||
},
|
},
|
||||||
"/apps/installed/sort/update": {
|
"/apps/installed/sort/update": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -679,8 +679,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "容器重命名 [name] => [newName]",
|
"formatZH": "容器重命名 [name] =\u003e [newName]",
|
||||||
"formatEN": "rename container [name] => [newName]"
|
"formatEN": "rename container [name] =\u003e [newName]"
|
||||||
},
|
},
|
||||||
"/containers/repo": {
|
"/containers/repo": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -831,8 +831,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
"formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||||
"formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
"formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||||
},
|
},
|
||||||
"/core/auth/expired/reset": {
|
"/core/auth/expired/reset": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -1716,8 +1716,8 @@
|
|||||||
"output_value": "name"
|
"output_value": "name"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
"formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||||
"formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
"formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||||
},
|
},
|
||||||
"/core/enterprise/users/del": {
|
"/core/enterprise/users/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -1918,8 +1918,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]",
|
"formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]",
|
||||||
"formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]"
|
"formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]"
|
||||||
},
|
},
|
||||||
"/core/settings/memo": {
|
"/core/settings/memo": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -1948,8 +1948,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改系统端口 => [serverPort]",
|
"formatZH": "修改系统端口 =\u003e [serverPort]",
|
||||||
"formatEN": "update system port => [serverPort]"
|
"formatEN": "update system port =\u003e [serverPort]"
|
||||||
},
|
},
|
||||||
"/core/settings/proxy/update": {
|
"/core/settings/proxy/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -1974,8 +1974,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改系统 ssl => [ssl]",
|
"formatZH": "修改系统 ssl =\u003e [ssl]",
|
||||||
"formatEN": "update system ssl => [ssl]"
|
"formatEN": "update system ssl =\u003e [ssl]"
|
||||||
},
|
},
|
||||||
"/core/settings/terminal/update": {
|
"/core/settings/terminal/update": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -1991,8 +1991,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改系统配置 [key] => [value]",
|
"formatZH": "修改系统配置 [key] =\u003e [value]",
|
||||||
"formatEN": "update system setting [key] => [value]"
|
"formatEN": "update system setting [key] =\u003e [value]"
|
||||||
},
|
},
|
||||||
"/core/settings/upgrade": {
|
"/core/settings/upgrade": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -2000,8 +2000,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新系统 => [version]",
|
"formatZH": "更新系统 =\u003e [version]",
|
||||||
"formatEN": "upgrade system => [version]"
|
"formatEN": "upgrade system =\u003e [version]"
|
||||||
},
|
},
|
||||||
"/core/xapp/appQRCode": {
|
"/core/xapp/appQRCode": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -2459,282 +2459,6 @@
|
|||||||
"formatZH": "同步 SSL 证书 [primaryDomain]",
|
"formatZH": "同步 SSL 证书 [primaryDomain]",
|
||||||
"formatEN": "sync SSL certificate [primaryDomain]"
|
"formatEN": "sync SSL certificate [primaryDomain]"
|
||||||
},
|
},
|
||||||
"/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]"
|
|
||||||
},
|
|
||||||
"/cronjobs": {
|
"/cronjobs": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"type",
|
"type",
|
||||||
@@ -2888,8 +2612,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "首页应用 [key] => 显示:[value]",
|
"formatZH": "首页应用 [key] =\u003e 显示:[value]",
|
||||||
"formatEN": "app launcher [key] => show: [value]"
|
"formatEN": "app launcher [key] =\u003e show: [value]"
|
||||||
},
|
},
|
||||||
"/dashboard/quick/change": {
|
"/dashboard/quick/change": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -3017,7 +2741,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
|
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
|
||||||
"formatEN": "The description of the mysql database [name] is modified => [description]"
|
"formatEN": "The description of the mysql database [name] is modified =\u003e [description]"
|
||||||
},
|
},
|
||||||
"/databases/grants": {
|
"/databases/grants": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3098,7 +2822,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]",
|
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]",
|
||||||
"formatEN": "The description of the mongodb database [name] is modified => [description]"
|
"formatEN": "The description of the mongodb database [name] is modified =\u003e [description]"
|
||||||
},
|
},
|
||||||
"/databases/mongodb/password": {
|
"/databases/mongodb/password": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3183,7 +2907,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]",
|
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]",
|
||||||
"formatEN": "The description of the postgresql database [name] is modified => [description]"
|
"formatEN": "The description of the postgresql database [name] is modified =\u003e [description]"
|
||||||
},
|
},
|
||||||
"/databases/pg/password": {
|
"/databases/pg/password": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3288,8 +3012,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]",
|
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]",
|
||||||
"formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]"
|
"formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]"
|
||||||
},
|
},
|
||||||
"/databases/variables/update": {
|
"/databases/variables/update": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -3325,8 +3049,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]",
|
"formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]",
|
||||||
"formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]"
|
"formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]"
|
||||||
},
|
},
|
||||||
"/files/chunkdownload": {
|
"/files/chunkdownload": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3416,8 +3140,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改权限 [path] => [mode]",
|
"formatZH": "修改权限 [path] =\u003e [mode]",
|
||||||
"formatEN": "Change mode [path] => [mode]"
|
"formatEN": "Change mode [path] =\u003e [mode]"
|
||||||
},
|
},
|
||||||
"/files/move": {
|
"/files/move": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3426,8 +3150,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "移动文件 [oldPaths] => [newPath]",
|
"formatZH": "移动文件 [oldPaths] =\u003e [newPath]",
|
||||||
"formatEN": "Move [oldPaths] => [newPath]"
|
"formatEN": "Move [oldPaths] =\u003e [newPath]"
|
||||||
},
|
},
|
||||||
"/files/owner": {
|
"/files/owner": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3437,8 +3161,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改用户/组 [path] => [user]/[group]",
|
"formatZH": "修改用户/组 [path] =\u003e [user]/[group]",
|
||||||
"formatEN": "Change owner [path] => [user]/[group]"
|
"formatEN": "Change owner [path] =\u003e [user]/[group]"
|
||||||
},
|
},
|
||||||
"/files/preview": {
|
"/files/preview": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3472,8 +3196,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "重命名 [oldName] => [newName]",
|
"formatZH": "重命名 [oldName] =\u003e [newName]",
|
||||||
"formatEN": "Rename [oldName] => [newName]"
|
"formatEN": "Rename [oldName] =\u003e [newName]"
|
||||||
},
|
},
|
||||||
"/files/save": {
|
"/files/save": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3530,8 +3254,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "下载 url => [path]/[name]",
|
"formatZH": "下载 url =\u003e [path]/[name]",
|
||||||
"formatEN": "Download url => [path]/[name]"
|
"formatEN": "Download url =\u003e [path]/[name]"
|
||||||
},
|
},
|
||||||
"/files/wget/process/remove": {
|
"/files/wget/process/remove": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3673,13 +3397,11 @@
|
|||||||
"formatEN": "batch update Docker port guard policies"
|
"formatEN": "batch update Docker port guard policies"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/docker/policies/delete/batch": {
|
"/hosts/firewall/docker/policies/delete/batch": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"uuids"
|
|
||||||
],
|
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "删除 Docker 端口防护策略 [uuids]",
|
"formatZH": "删除 Docker 端口防护策略",
|
||||||
"formatEN": "delete Docker port guard policies [uuids]"
|
"formatEN": "delete Docker port guard policies"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/family/operate": {
|
"/hosts/firewall/family/operate": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3687,10 +3409,10 @@
|
|||||||
"family",
|
"family",
|
||||||
"operation"
|
"operation"
|
||||||
],
|
],
|
||||||
"formatEN": "[operation] [subsystem] [family] firewall chains",
|
|
||||||
"formatZH": "[operation] [subsystem] [family] 防火墙链",
|
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": []
|
"beforeFunctions": [],
|
||||||
|
"formatZH": "[operation] [subsystem] [family] 防火墙链",
|
||||||
|
"formatEN": "[operation] [subsystem] [family] firewall chains"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/filter/operate": {
|
"/hosts/firewall/filter/operate": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3739,13 +3461,11 @@
|
|||||||
"formatEN": "delete firewall rules"
|
"formatEN": "delete firewall rules"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/rules/reorder": {
|
"/hosts/firewall/rules/reorder": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"instanceKey"
|
|
||||||
],
|
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "调整防火墙规则顺序 [instanceKey]",
|
"formatZH": "调整防火墙规则顺序",
|
||||||
"formatEN": "reorder firewall rule [instanceKey]"
|
"formatEN": "reorder firewall rule"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/rules/reset": {
|
"/hosts/firewall/rules/reset": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
@@ -3755,22 +3475,20 @@
|
|||||||
"formatEN": "reset firewall rules"
|
"formatEN": "reset firewall rules"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/rules/update": {
|
"/hosts/firewall/rules/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [],
|
||||||
"instanceKey"
|
|
||||||
],
|
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新防火墙规则 [instanceKey]",
|
"formatZH": "更新防火墙规则",
|
||||||
"formatEN": "update firewall rule [instanceKey]"
|
"formatEN": "update firewall rule"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/settings/ipv6": {
|
"/hosts/firewall/settings/ipv6": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
"status"
|
"status"
|
||||||
],
|
],
|
||||||
"formatEN": "Set firewall IPv6 support to [status]",
|
|
||||||
"formatZH": "设置防火墙 IPv6 支持为 [status]",
|
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": []
|
"beforeFunctions": [],
|
||||||
|
"formatZH": "设置防火墙 IPv6 支持为 [status]",
|
||||||
|
"formatEN": "Set firewall IPv6 support to [status]"
|
||||||
},
|
},
|
||||||
"/hosts/firewall/settings/operate": {
|
"/hosts/firewall/settings/operate": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3812,11 +3530,13 @@
|
|||||||
"formatEN": "update firewall port whitelist"
|
"formatEN": "update firewall port whitelist"
|
||||||
},
|
},
|
||||||
"/hosts/monitor/clean": {
|
"/hosts/monitor/clean": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "清空监控数据",
|
"formatZH": "清空 [type] 监控数据",
|
||||||
"formatEN": "clean monitor datas"
|
"formatEN": "clean [type] monitoring data"
|
||||||
},
|
},
|
||||||
"/hosts/monitor/setting/update": {
|
"/hosts/monitor/setting/update": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -3888,8 +3608,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改 SSH 配置 [key] => [newValue]",
|
"formatZH": "修改 SSH 配置 [key] =\u003e [newValue]",
|
||||||
"formatEN": "update SSH setting [key] => [newValue]"
|
"formatEN": "update SSH setting [key] =\u003e [newValue]"
|
||||||
},
|
},
|
||||||
"/hosts/tool/config/set": {
|
"/hosts/tool/config/set": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4150,7 +3870,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "快照 [name] 描述信息修改 [description]",
|
"formatZH": "快照 [name] 描述信息修改 [description]",
|
||||||
"formatEN": "The description of the snapshot [name] is modified => [description]"
|
"formatEN": "The description of the snapshot [name] is modified =\u003e [description]"
|
||||||
},
|
},
|
||||||
"/settings/snapshot/import": {
|
"/settings/snapshot/import": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4242,8 +3962,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改系统配置 [key] => [value]",
|
"formatZH": "修改系统配置 [key] =\u003e [value]",
|
||||||
"formatEN": "update system setting [key] => [value]"
|
"formatEN": "update system setting [key] =\u003e [value]"
|
||||||
},
|
},
|
||||||
"/toolbox/clam": {
|
"/toolbox/clam": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4361,8 +4081,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改主机参数 [key] => [value]",
|
"formatZH": "修改主机参数 [key] =\u003e [value]",
|
||||||
"formatEN": "update device conf [key] => [value]"
|
"formatEN": "update device conf [key] =\u003e [value]"
|
||||||
},
|
},
|
||||||
"/toolbox/device/update/host": {
|
"/toolbox/device/update/host": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4371,8 +4091,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改主机 Host [key] => [value]",
|
"formatZH": "修改主机 Host [key] =\u003e [value]",
|
||||||
"formatEN": "update device host [key] => [value]"
|
"formatEN": "update device host [key] =\u003e [value]"
|
||||||
},
|
},
|
||||||
"/toolbox/device/update/swap": {
|
"/toolbox/device/update/swap": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4400,8 +4120,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "修改 Fail2ban 配置 [key] => [value]",
|
"formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]",
|
||||||
"formatEN": "update fail2ban conf [key] => [value]"
|
"formatEN": "update fail2ban conf [key] =\u003e [value]"
|
||||||
},
|
},
|
||||||
"/toolbox/ftp": {
|
"/toolbox/ftp": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -4660,8 +4380,8 @@
|
|||||||
"output_value": "domain"
|
"output_value": "domain"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"formatZH": "修改默认 server => [domain]",
|
"formatZH": "修改默认 server =\u003e [domain]",
|
||||||
"formatEN": "Change default server => [domain]"
|
"formatEN": "Change default server =\u003e [domain]"
|
||||||
},
|
},
|
||||||
"/websites/del": {
|
"/websites/del": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -5424,8 +5144,8 @@
|
|||||||
],
|
],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
"beforeFunctions": [],
|
"beforeFunctions": [],
|
||||||
"formatZH": "更新防篡改信息 [website][path] => [status]",
|
"formatZH": "更新防篡改信息 [website][path] =\u003e [status]",
|
||||||
"formatEN": "update tamper info [website][path] => [status]"
|
"formatEN": "update tamper info [website][path] =\u003e [status]"
|
||||||
},
|
},
|
||||||
"/xpack/vllm/command-template/create": {
|
"/xpack/vllm/command-template/create": {
|
||||||
"bodyKeys": [
|
"bodyKeys": [
|
||||||
@@ -5504,6 +5224,282 @@
|
|||||||
"formatZH": "更新 vLLM [name]",
|
"formatZH": "更新 vLLM [name]",
|
||||||
"formatEN": "update 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": {
|
"/xpack/waf/block/export": {
|
||||||
"bodyKeys": [],
|
"bodyKeys": [],
|
||||||
"paramKeys": [],
|
"paramKeys": [],
|
||||||
|
|||||||
+14
-14
@@ -30,7 +30,7 @@ require (
|
|||||||
github.com/pkg/sftp v1.13.11
|
github.com/pkg/sftp v1.13.11
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/russellhaering/goxmldsig v1.6.1
|
github.com/russellhaering/goxmldsig v1.6.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/sirupsen/logrus v1.10.2
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/soheilhy/cmux v0.1.5
|
github.com/soheilhy/cmux v0.1.5
|
||||||
@@ -39,13 +39,13 @@ require (
|
|||||||
github.com/swaggo/files/v2 v2.0.2
|
github.com/swaggo/files/v2 v2.0.2
|
||||||
github.com/swaggo/swag v1.16.6
|
github.com/swaggo/swag v1.16.6
|
||||||
github.com/xlzd/gotp v0.1.0
|
github.com/xlzd/gotp v0.1.0
|
||||||
golang.org/x/crypto v0.56.0
|
golang.org/x/crypto v0.58.0
|
||||||
golang.org/x/image v0.45.0
|
golang.org/x/image v0.45.0
|
||||||
golang.org/x/net v0.58.0
|
golang.org/x/net v0.60.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/sys v0.47.0
|
golang.org/x/sys v0.49.0
|
||||||
golang.org/x/term v0.45.0
|
golang.org/x/term v0.47.0
|
||||||
golang.org/x/text v0.41.0
|
golang.org/x/text v0.43.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/gorm v1.31.2
|
gorm.io/gorm v1.31.2
|
||||||
@@ -61,7 +61,7 @@ require (
|
|||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // 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/f-amaral/go-async v0.3.0 // indirect
|
github.com/f-amaral/go-async v0.3.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
@@ -110,7 +110,7 @@ require (
|
|||||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/phpdave11/gofpdf v1.4.3 // indirect
|
github.com/phpdave11/gofpdf v1.4.3 // 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/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.1 // indirect
|
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
@@ -120,18 +120,18 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/tinylib/msgp v1.6.4 // indirect
|
github.com/tinylib/msgp v1.6.4 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.9.2 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
golang.org/x/arch v0.26.0 // indirect
|
golang.org/x/arch v0.26.0 // indirect
|
||||||
golang.org/x/mod v0.40.0 // indirect
|
golang.org/x/mod v0.41.0 // indirect
|
||||||
golang.org/x/sync v0.22.0 // indirect
|
golang.org/x/sync v0.24.0 // indirect
|
||||||
golang.org/x/tools v0.49.0 // indirect
|
golang.org/x/tools v0.51.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
modernc.org/libc v1.72.0 // indirect
|
modernc.org/libc v1.72.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
|||||||
+40
@@ -35,6 +35,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
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.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/f-amaral/go-async v0.3.0 h1:h4kLsX7aKfdWaHvV0lf+/EE3OIeCzyeDYJDb/vDZUyg=
|
github.com/f-amaral/go-async v0.3.0 h1:h4kLsX7aKfdWaHvV0lf+/EE3OIeCzyeDYJDb/vDZUyg=
|
||||||
github.com/f-amaral/go-async v0.3.0/go.mod h1:Hz5Qr6DAWpbTTUjytnrg1WIsDgS7NtOei5y8SipYS7U=
|
github.com/f-amaral/go-async v0.3.0/go.mod h1:Hz5Qr6DAWpbTTUjytnrg1WIsDgS7NtOei5y8SipYS7U=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
@@ -219,6 +221,8 @@ github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hs
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
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-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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||||
@@ -237,6 +241,10 @@ github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88ee
|
|||||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||||
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
|
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.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/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
@@ -279,8 +287,12 @@ github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
|||||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||||
|
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||||
|
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||||
|
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||||
|
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
@@ -293,6 +305,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
|
|||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro=
|
go.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
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.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
@@ -304,19 +318,31 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
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.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/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
|
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/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
|
||||||
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
||||||
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
|
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
|
||||||
|
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||||
|
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
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.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.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
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.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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -324,17 +350,31 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
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.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/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
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.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
|
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||||
|
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||||
|
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
|
||||||
|
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
|
||||||
golang.org/x/text v0.3.0/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
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.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.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||||
|
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
|
||||||
|
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
|||||||
+15
-4
@@ -230,6 +230,10 @@ func (c *CommandHelper) buildPipeCommands(ctx context.Context, commands []PipeCo
|
|||||||
cmdItem.SysProcAttr = &syscall.SysProcAttr{
|
cmdItem.SysProcAttr = &syscall.SysProcAttr{
|
||||||
Setpgid: true,
|
Setpgid: true,
|
||||||
}
|
}
|
||||||
|
cmdItem.Cancel = func() error {
|
||||||
|
_ = syscall.Kill(-cmdItem.Process.Pid, syscall.SIGKILL)
|
||||||
|
return cmdItem.Process.Kill()
|
||||||
|
}
|
||||||
cmds = append(cmds, cmdItem)
|
cmds = append(cmds, cmdItem)
|
||||||
}
|
}
|
||||||
return cmds
|
return cmds
|
||||||
@@ -305,6 +309,12 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
|
|||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
Setpgid: true,
|
Setpgid: true,
|
||||||
}
|
}
|
||||||
|
if newContext != nil {
|
||||||
|
cmd.Cancel = func() error {
|
||||||
|
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||||
|
return cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
customWriter := &CustomWriter{taskItem: c.taskItem}
|
customWriter := &CustomWriter{taskItem: c.taskItem}
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
@@ -357,11 +367,12 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
|
|||||||
}()
|
}()
|
||||||
select {
|
select {
|
||||||
case err := <-done:
|
case err := <-done:
|
||||||
if c.preserveErrorCause && newContext != nil && newContext.Err() != nil {
|
if newContext != nil && newContext.Err() != nil {
|
||||||
if cmd.Process != nil && cmd.Process.Pid > 0 {
|
resultErr := c.pipeResultErr(newContext, err)
|
||||||
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
if c.preserveErrorCause {
|
||||||
|
resultErr = &commandError{message: resultErr.Error(), cause: newContext.Err()}
|
||||||
}
|
}
|
||||||
return "", newContext.Err()
|
return "", resultErr
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out, resultErr := handleErr(&stdout, &stderr, c.IgnoreExist1, err)
|
out, resultErr := handleErr(&stdout, &stderr, c.IgnoreExist1, err)
|
||||||
|
|||||||
+25
-12
@@ -13,20 +13,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func LoadDBConnByPath(fullPath, dbName string) *gorm.DB {
|
func LoadDBConnByPath(fullPath, dbName string) *gorm.DB {
|
||||||
if _, err := CreateDirWhenNotExist(true, path.Dir(fullPath)); err != nil {
|
|
||||||
panic(fmt.Errorf("init db dir failed, err: %v", err))
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(fullPath); err != nil {
|
|
||||||
f, err := os.Create(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("init %s db file failed, err: %v", dbName, err))
|
|
||||||
}
|
|
||||||
_ = f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := GetDBWithPath(fullPath)
|
db, err := GetDBWithPath(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(fmt.Errorf("init %s db failed, err: %w", dbName, err))
|
||||||
}
|
}
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
@@ -40,6 +29,30 @@ func CloseDB(db *gorm.DB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetDBWithPath(dbPath string) (*gorm.DB, error) {
|
func GetDBWithPath(dbPath string) (*gorm.DB, error) {
|
||||||
|
dbDir := path.Dir(dbPath)
|
||||||
|
if err := os.MkdirAll(dbDir, 0700); err != nil {
|
||||||
|
return nil, fmt.Errorf("init db dir failed, err: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.Chmod(dbDir, 0700); err != nil {
|
||||||
|
return nil, fmt.Errorf("set db dir permissions failed, err: %w", err)
|
||||||
|
}
|
||||||
|
file, err := os.OpenFile(dbPath, os.O_CREATE|os.O_RDWR, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init db file failed, err: %w", err)
|
||||||
|
}
|
||||||
|
if err := file.Chmod(0600); err != nil {
|
||||||
|
_ = file.Close()
|
||||||
|
return nil, fmt.Errorf("set db file permissions failed, err: %w", err)
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, suffix := range []string{"-wal", "-shm", "-journal"} {
|
||||||
|
if err := os.Chmod(dbPath+suffix, 0600); err != nil && !os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("set db sidecar permissions failed, err: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
Logger: newLogger(),
|
Logger: newLogger(),
|
||||||
|
|||||||
Generated
+331
-1372
File diff suppressed because it is too large
Load Diff
+18
-14
@@ -29,16 +29,16 @@
|
|||||||
"@novnc/novnc": "^1.7.0",
|
"@novnc/novnc": "^1.7.0",
|
||||||
"@vue-office/docx": "^1.6.2",
|
"@vue-office/docx": "^1.6.2",
|
||||||
"@vue-office/excel": "^1.7.8",
|
"@vue-office/excel": "^1.7.8",
|
||||||
"@vueuse/core": "^14.4.0",
|
"@vueuse/core": "^15.0.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"anser": "^2.3.5",
|
"anser": "^2.3.5",
|
||||||
"axios": "^1.18.0",
|
"axios": "^1.20.0",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dompurify": "^3.4.13",
|
"dompurify": "^3.4.16",
|
||||||
"echarts": "^6.1.0",
|
"echarts": "^6.1.0",
|
||||||
"element-plus": "2.14.4",
|
"element-plus": "2.14.7",
|
||||||
"js-base64": "^3.9.1",
|
"js-base64": "^3.9.1",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash-es": "^4.18.1",
|
"lodash-es": "^4.18.1",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"screenfull": "^6.0.2",
|
"screenfull": "^6.0.2",
|
||||||
"sortablejs": "^1.15.7",
|
"sortablejs": "^1.15.7",
|
||||||
"uuid": "^14.0.1",
|
"uuid": "^14.0.1",
|
||||||
"vue": "^3.5.42",
|
"vue": "^3.5.43",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-codemirror": "^6.1.1",
|
"vue-codemirror": "^6.1.1",
|
||||||
"vue-demi": "^0.14.6",
|
"vue-demi": "^0.14.6",
|
||||||
@@ -63,22 +63,22 @@
|
|||||||
"@tailwindcss/postcss": "^4.3.3",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@types/node": "^26.6.3",
|
"@types/node": "^26.6.3",
|
||||||
"@types/uuid": "^11.0.0",
|
"@types/uuid": "^11.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
"@typescript-eslint/eslint-plugin": "^8.71.2",
|
||||||
"@typescript-eslint/parser": "^8.68.0",
|
"@typescript-eslint/parser": "^8.71.2",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.10",
|
||||||
"@vitejs/plugin-vue-jsx": "^5.1.6",
|
"@vitejs/plugin-vue-jsx": "^5.1.6",
|
||||||
"@vue/compiler-sfc": "^3.5.42",
|
"@vue/compiler-sfc": "^3.5.43",
|
||||||
"autoprefixer": "^10.6.1",
|
"autoprefixer": "^10.6.1",
|
||||||
"esbuild": "^0.28.2",
|
"esbuild": "^0.28.2",
|
||||||
"eslint": "^10.8.0",
|
"eslint": "^10.8.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"eslint-plugin-vue": "^10.10.0",
|
"eslint-plugin-vue": "^10.10.0",
|
||||||
"lint-staged": "^17.0.7",
|
"lint-staged": "^17.6.0",
|
||||||
"postcss": "^8.5.14",
|
"postcss": "^8.5.14",
|
||||||
"postcss-html": "^2.0.0",
|
"postcss-html": "^2.0.0",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"rollup-plugin-visualizer": "^7.0.1",
|
"rollup-plugin-visualizer": "^7.1.1",
|
||||||
"sass": "^1.105.0",
|
"sass": "^1.105.0",
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
@@ -100,7 +100,11 @@
|
|||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"monaco-editor": {
|
"monaco-editor": {
|
||||||
"dompurify": "3.4.13"
|
"dompurify": "3.4.16"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"allowScripts": {
|
||||||
|
"@vue-office/docx@1.6.3": true,
|
||||||
|
"@vue-office/excel@1.7.14": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class RequestHttp {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.service.interceptors.response.use(
|
this.service.interceptors.response.use(
|
||||||
(response: AxiosResponse) => {
|
async (response: AxiosResponse) => {
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
const { data } = response;
|
const { data } = response;
|
||||||
const authResult = handleAuthResponseCode(data, { showRBACMessage: true });
|
const authResult = handleAuthResponseCode(data, { showRBACMessage: true });
|
||||||
@@ -100,9 +100,9 @@ class RequestHttp {
|
|||||||
return Promise.reject(data);
|
return Promise.reject(data);
|
||||||
}
|
}
|
||||||
if (data.code == ResultEnum.NODE_UNBIND) {
|
if (data.code == ResultEnum.NODE_UNBIND) {
|
||||||
changeToLocal();
|
await changeToLocal();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
return;
|
return Promise.reject(data);
|
||||||
}
|
}
|
||||||
if (data.code == ResultEnum.ERR_GLOBAL_LOADING) {
|
if (data.code == ResultEnum.ERR_GLOBAL_LOADING) {
|
||||||
globalStore.isLoading = true;
|
globalStore.isLoading = true;
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export namespace Firewall {
|
|||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
bound: boolean;
|
bound: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
forwardPolicy?: 'ACCEPT' | 'DROP';
|
|
||||||
raInterfaces?: string[];
|
raInterfaces?: string[];
|
||||||
}
|
}
|
||||||
export interface BackendGroup {
|
export interface BackendGroup {
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ export namespace Website {
|
|||||||
email: string;
|
email: string;
|
||||||
url: string;
|
url: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
keyType: string;
|
||||||
useProxy: boolean;
|
useProxy: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,8 +144,13 @@ export const imagePull = (params: Container.ImagePull) => {
|
|||||||
export const imagePush = (params: Container.ImagePush) => {
|
export const imagePush = (params: Container.ImagePush) => {
|
||||||
return http.post<string>(`/containers/image/push`, params);
|
return http.post<string>(`/containers/image/push`, params);
|
||||||
};
|
};
|
||||||
export const imageLoad = (params: Container.ImageLoad) => {
|
export const imageLoad = (params: Container.ImageLoad, currentNode?: string) => {
|
||||||
return http.post(`/containers/image/load`, params, TimeoutEnum.T_10M);
|
return http.post(
|
||||||
|
`/containers/image/load`,
|
||||||
|
params,
|
||||||
|
TimeoutEnum.T_10M,
|
||||||
|
currentNode ? { CurrentNode: currentNode } : undefined,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
export const imageSave = (params: Container.ImageSave) => {
|
export const imageSave = (params: Container.ImageSave) => {
|
||||||
return http.post(`/containers/image/save`, params, TimeoutEnum.T_10M);
|
return http.post(`/containers/image/save`, params, TimeoutEnum.T_10M);
|
||||||
|
|||||||
@@ -193,8 +193,10 @@ export const redisPersistenceConf = (type: string, database: string) => {
|
|||||||
export const checkRedisCli = () => {
|
export const checkRedisCli = () => {
|
||||||
return http.get<boolean>(`/databases/redis/check`);
|
return http.get<boolean>(`/databases/redis/check`);
|
||||||
};
|
};
|
||||||
export const installRedisCli = () => {
|
export const installRedisCli = (taskID: string, node: string) => {
|
||||||
return http.post(`/databases/redis/install/cli`, {}, TimeoutEnum.T_5M);
|
return http.post<string>(`/databases/redis/install/cli?operateNode=${encodeURIComponent(node)}`, {
|
||||||
|
taskID,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
export const changeRedisPassword = (database: string, password: string) => {
|
export const changeRedisPassword = (database: string, password: string) => {
|
||||||
if (password) {
|
if (password) {
|
||||||
|
|||||||
@@ -126,14 +126,14 @@ const styleVars = computed(() => ({
|
|||||||
|
|
||||||
const terminalElement = ref<HTMLDivElement | null>(null);
|
const terminalElement = ref<HTMLDivElement | null>(null);
|
||||||
let eventSource: EventSource | null = null;
|
let eventSource: EventSource | null = null;
|
||||||
|
let searchVersion = 0;
|
||||||
let term: Terminal | null = null;
|
let term: Terminal | null = null;
|
||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
let onScrollDisposable: { dispose: () => void } | null = null;
|
|
||||||
const MAX_VIEW_LINES = 20000;
|
const MAX_VIEW_LINES = 20000;
|
||||||
const followBottom = ref(true);
|
let followBottom = true;
|
||||||
|
|
||||||
const logSearch = reactive({
|
const logSearch = reactive({
|
||||||
isWatch: props.defaultFollow ? true : true,
|
isWatch: true,
|
||||||
isShowTimestamp: props.defaultIsShowTimestamp,
|
isShowTimestamp: props.defaultIsShowTimestamp,
|
||||||
container: '',
|
container: '',
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
@@ -168,6 +168,7 @@ const timeOptions = ref([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const stopListening = () => {
|
const stopListening = () => {
|
||||||
|
searchVersion++;
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
eventSource = null;
|
eventSource = null;
|
||||||
@@ -175,26 +176,13 @@ const stopListening = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clearTerminal = () => {
|
const clearTerminal = () => {
|
||||||
term?.reset();
|
// Queue the reset after pending writes so old logs cannot appear in a new search.
|
||||||
followBottom.value = true;
|
term?.write('\x1bc');
|
||||||
|
followBottom = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const writeLogLine = (data: string) => {
|
const writeLogLine = (data: string) => {
|
||||||
if (!term) return;
|
term?.writeln(data);
|
||||||
term.writeln(data);
|
|
||||||
if (followBottom.value) {
|
|
||||||
term.scrollToBottom();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindXTermEvents = () => {
|
|
||||||
if (!term) return;
|
|
||||||
onScrollDisposable?.dispose();
|
|
||||||
onScrollDisposable = term.onScroll(() => {
|
|
||||||
if (!term) return;
|
|
||||||
const active = term.buffer.active;
|
|
||||||
followBottom.value = active.baseY + active.cursorY >= active.length - 2;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEventSourceAuthError = (message: string) => {
|
const showEventSourceAuthError = (message: string) => {
|
||||||
@@ -235,11 +223,15 @@ const initTerminal = () => {
|
|||||||
term.open(terminalElement.value);
|
term.open(terminalElement.value);
|
||||||
term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
bindXTermEvents();
|
// Terminal.dispose() also disposes these listeners.
|
||||||
};
|
term.onScroll(() => {
|
||||||
|
if (!term) return;
|
||||||
const handleClose = async () => {
|
const active = term.buffer.active;
|
||||||
stopListening();
|
followBottom = active.viewportY >= active.baseY;
|
||||||
|
});
|
||||||
|
term.onWriteParsed(() => {
|
||||||
|
if (followBottom) term?.scrollToBottom();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchLogs = async () => {
|
const searchLogs = async () => {
|
||||||
@@ -248,6 +240,7 @@ const searchLogs = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stopListening();
|
stopListening();
|
||||||
|
const version = searchVersion;
|
||||||
clearTerminal();
|
clearTerminal();
|
||||||
|
|
||||||
let currentNode = globalCurrentNode.value;
|
let currentNode = globalCurrentNode.value;
|
||||||
@@ -261,15 +254,19 @@ const searchLogs = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authError = await checkStreamAuth(url, currentNode);
|
const authError = await checkStreamAuth(url, currentNode);
|
||||||
|
if (version !== searchVersion) return;
|
||||||
if (authError) {
|
if (authError) {
|
||||||
showEventSourceAuthError(authError);
|
showEventSourceAuthError(authError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
eventSource = new EventSource(url);
|
const source = new EventSource(url);
|
||||||
eventSource.onmessage = (event: MessageEvent) => {
|
eventSource = source;
|
||||||
|
source.onmessage = (event: MessageEvent) => {
|
||||||
|
if (version !== searchVersion) return;
|
||||||
writeLogLine(event.data);
|
writeLogLine(event.data);
|
||||||
};
|
};
|
||||||
eventSource.onerror = (event: MessageEvent) => {
|
source.onerror = (event: MessageEvent) => {
|
||||||
|
if (version !== searchVersion) return;
|
||||||
stopListening();
|
stopListening();
|
||||||
if (event.data && event.data != '') {
|
if (event.data && event.data != '') {
|
||||||
MsgError(event.data);
|
MsgError(event.data);
|
||||||
@@ -353,8 +350,6 @@ onMounted(() => {
|
|||||||
logSearch.resource = props.resource;
|
logSearch.resource = props.resource;
|
||||||
|
|
||||||
logSearch.tail = 100;
|
logSearch.tail = 100;
|
||||||
logSearch.mode = 'all';
|
|
||||||
logSearch.isWatch = true;
|
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initTerminal();
|
initTerminal();
|
||||||
@@ -369,8 +364,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
handleClose();
|
stopListening();
|
||||||
onScrollDisposable?.dispose();
|
|
||||||
if (term) {
|
if (term) {
|
||||||
term.dispose();
|
term.dispose();
|
||||||
term = null;
|
term = null;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { onActivated, ref, watch } from 'vue';
|
||||||
defineOptions({ name: 'TableSearch' });
|
defineOptions({ name: 'TableSearch' });
|
||||||
|
|
||||||
const emit = defineEmits(['search', 'update:searchName']);
|
const emit = defineEmits(['search', 'update:searchName']);
|
||||||
@@ -42,6 +42,10 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
searchInfo.value = props.searchName;
|
||||||
|
});
|
||||||
|
|
||||||
const search = () => {
|
const search = () => {
|
||||||
emit('update:searchName', searchInfo.value);
|
emit('update:searchName', searchInfo.value);
|
||||||
emit('search');
|
emit('search');
|
||||||
|
|||||||
@@ -167,7 +167,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, useAttrs } from 'vue';
|
import { computed, onDeactivated, onMounted, ref, useAttrs } from 'vue';
|
||||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||||
import { flattenVNodes, type FuTableOperationButton } from '@/components/table/shared';
|
import { flattenVNodes, type FuTableOperationButton } from '@/components/table/shared';
|
||||||
import { useCardColumns } from './useCardColumns';
|
import { useCardColumns } from './useCardColumns';
|
||||||
@@ -459,6 +459,8 @@ watch([currentViewMode, tableData, () => props.syncCardContentHeight], scheduleC
|
|||||||
flush: 'post',
|
flush: 'post',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDeactivated(clearSelects);
|
||||||
|
|
||||||
watch([() => props.paginationConfig?.currentPage, () => props.paginationConfig?.pageSize], clearSelects, {
|
watch([() => props.paginationConfig?.currentPage, () => props.paginationConfig?.pageSize], clearSelects, {
|
||||||
flush: 'sync',
|
flush: 'sync',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { reactive, type UnwrapNestedRefs } from 'vue';
|
import { reactive, type UnwrapNestedRefs } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
|
||||||
import { getPageState } from '@/utils/page-state-cache';
|
|
||||||
|
|
||||||
|
/** A new page visit starts from its defaults instead of restoring the previous page number. */
|
||||||
export const usePageState = <T extends object>(factory: () => T): UnwrapNestedRefs<T> => {
|
export const usePageState = <T extends object>(factory: () => T): UnwrapNestedRefs<T> => {
|
||||||
const route = useRoute();
|
return reactive(factory());
|
||||||
const { currentNode } = useGlobalStore();
|
|
||||||
const key = `${currentNode.value}:${String(route.name || route.path)}`;
|
|
||||||
return reactive(getPageState(key, factory));
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { watch, type Ref } from 'vue';
|
|
||||||
import GlobalStore from '@/store/modules/global';
|
|
||||||
|
|
||||||
type SearchValue = string | number | undefined;
|
|
||||||
type SearchFields = Record<string, Ref<SearchValue>>;
|
|
||||||
|
|
||||||
export const useSearchPersistence = (page: string, fields: SearchFields, scope?: () => SearchValue) => {
|
|
||||||
const { currentNode } = storeToRefs(GlobalStore());
|
|
||||||
const defaults = Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value]));
|
|
||||||
let initialized = false;
|
|
||||||
let restoring = false;
|
|
||||||
let activeKey: string | undefined;
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => {
|
|
||||||
const context = scope?.();
|
|
||||||
if (scope && context === undefined) return undefined;
|
|
||||||
return `1panel:search:v1:${JSON.stringify([currentNode.value, page, context ?? ''])}`;
|
|
||||||
},
|
|
||||||
(key) => {
|
|
||||||
activeKey = key;
|
|
||||||
let saved: Record<string, unknown> = {};
|
|
||||||
try {
|
|
||||||
const parsed = activeKey ? JSON.parse(localStorage.getItem(activeKey) || '{}') : {};
|
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) saved = parsed;
|
|
||||||
} catch {}
|
|
||||||
restoring = true;
|
|
||||||
for (const [name, field] of Object.entries(fields)) {
|
|
||||||
const value = saved[name];
|
|
||||||
const initial = defaults[name];
|
|
||||||
let fallback = initial;
|
|
||||||
if (initialized) {
|
|
||||||
fallback = typeof initial === 'string' ? '' : undefined;
|
|
||||||
}
|
|
||||||
const valid =
|
|
||||||
(typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))) &&
|
|
||||||
(fallback === undefined || typeof value === typeof fallback);
|
|
||||||
if (value === null) {
|
|
||||||
field.value = typeof initial === 'string' ? '' : undefined;
|
|
||||||
} else if (valid) {
|
|
||||||
field.value = value as SearchValue;
|
|
||||||
} else {
|
|
||||||
field.value = fallback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
restoring = false;
|
|
||||||
initialized = true;
|
|
||||||
},
|
|
||||||
{ immediate: true, flush: 'sync' },
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value ?? null])),
|
|
||||||
(value) => {
|
|
||||||
if (restoring || !activeKey) return;
|
|
||||||
try {
|
|
||||||
localStorage.setItem(activeKey, JSON.stringify(value));
|
|
||||||
} catch {}
|
|
||||||
},
|
|
||||||
{ flush: 'sync' },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { onActivated, onDeactivated } from 'vue';
|
||||||
|
import GlobalStore from '@/store/modules/global';
|
||||||
|
import { bindSearchReset, type SearchFields } from '@/utils/search-reset';
|
||||||
|
|
||||||
|
interface SearchResetOptions {
|
||||||
|
scope?: () => string | number | undefined;
|
||||||
|
onRevisit: () => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSearchReset = (fields: SearchFields, options: SearchResetOptions) => {
|
||||||
|
const { currentNode } = storeToRefs(GlobalStore());
|
||||||
|
const reset = bindSearchReset(options.scope ? [currentNode, options.scope] : [currentNode], fields);
|
||||||
|
let deactivated = false;
|
||||||
|
onDeactivated(() => {
|
||||||
|
deactivated = true;
|
||||||
|
});
|
||||||
|
onActivated(() => {
|
||||||
|
if (!deactivated) return;
|
||||||
|
deactivated = false;
|
||||||
|
reset();
|
||||||
|
return options.onRevisit();
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -216,6 +216,10 @@ export const DNSTypes = [
|
|||||||
label: 'NameSilo',
|
label: 'NameSilo',
|
||||||
value: 'NameSilo',
|
value: 'NameSilo',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Netcup',
|
||||||
|
value: 'Netcup',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'NameCheap',
|
label: 'NameCheap',
|
||||||
value: 'NameCheap',
|
value: 'NameCheap',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user