Compare commits

..
Author SHA1 Message Date
guoyuqi 262618a4a9 fix: fix container list 2026-10-08 17:45:09 +08:00
guoyuqi b374b70e71 fix: fix container list 2026-10-08 17:44:29 +08:00
guoyuqi 4ea3f7e4b8 fix: fix clean network 2026-10-08 17:44:29 +08:00
guoyuqi a917880cac fix: fix clean network 2026-10-08 17:44:29 +08:00
guoyuqi d41dd382ab fix: fix container list 2026-10-08 17:44:29 +08:00
Yuki Guoandssongliu 2701af9054 refactor: select bind (#13993)
* refactor: select bind

* refactor: simplify table selection state handling

---------

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

* refactor: simplify search persistence

---------

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

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

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

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

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

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

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

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

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

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

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

* chore(monitor): remove monitor test fixtures

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-09-29 15:34:16 +08:00
ssongliu d0183f460e feat: add vLLM monitoring (#13942) 2026-09-29 15:19:30 +08:00
ssongliu d27d6db1ea feat: enhance native GPU NPU and XPU monitoring (#13939) 2026-09-28 18:32:12 +08:00
蘭 d2bb3813d9 fix: enhance SSH login alert by adding local IP addresses to success login whitelist (#13920) 2026-09-24 18:23:59 +08:00
蘭 cac73e6d45 fix: add success alert message for cron jobs and enhance alert handling (#13919) 2026-09-24 18:23:39 +08:00
ssongliu 4712ceaf2c fix: allow read-only firewall APIs in demo mode (#13914) 2026-09-24 14:01:05 +08:00
185 changed files with 7999 additions and 2129 deletions
+18 -3
View File
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
return
}
total, list, err := containerService.Page(req)
total, list, err := containerService.Page(c.Request.Context(), req)
if err != nil {
helper.InternalServer(c, err)
return
@@ -249,9 +249,10 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
// @Success 200 {object} dto.ContainerStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Param containersOnly query boolean false "Only count containers"
// @Router /containers/status [get]
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
data, err := containerService.LoadStatus()
data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true")
if err != nil {
helper.InternalServer(c, err)
return
@@ -415,9 +416,14 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
// @Success 200 {array} dto.ContainerListStats
// @Security ApiKeyAuth
// @Security Timestamp
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
// @Router /containers/list/stats [get]
func (b *BaseApi) ContainerListStats(c *gin.Context) {
data, err := containerService.ContainerListStats()
var ids []string
if _, supplied := c.Request.URL.Query()["ids"]; supplied {
ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' })
}
data, err := containerService.ContainerListStats(c.Request.Context(), ids)
if err != nil {
helper.InternalServer(c, err)
return
@@ -964,3 +970,12 @@ func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
containerService.StreamLogs(c, streamLog)
}
func (b *BaseApi) CleanNetworks(c *gin.Context) {
result, err := containerService.CleanNetworks()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
+10 -23
View File
@@ -1,13 +1,9 @@
package v2
import (
"sort"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/net"
)
// @Tags Monitor
@@ -32,14 +28,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
}
// @Tags Monitor
// @Summary Clean monitor data
// @Summary Clean host or GPU monitor data
// @Param request body dto.MonitorClean true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/monitor/clean [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"}
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 [type] 监控数据","formatEN":"clean [type] monitoring data"}
func (b *BaseApi) CleanMonitor(c *gin.Context) {
if err := monitorService.CleanData(); err != nil {
var req dto.MonitorClean
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := monitorService.CleanData(req.Type); err != nil {
helper.InternalServer(c, err)
return
}
@@ -91,14 +92,7 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
// @Security Timestamp
// @Router /hosts/monitor/netoptions [get]
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
netStat, _ := net.IOCounters(true)
var options []string
options = append(options, "all")
for _, net := range netStat {
options = append(options, net.Name)
}
sort.Strings(options)
helper.SuccessWithData(c, options)
helper.SuccessWithData(c, monitorService.LoadNetworkOptions())
}
// @Tags Monitor
@@ -108,12 +102,5 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
// @Security Timestamp
// @Router /hosts/monitor/iooptions [get]
func (b *BaseApi) GetIOOptions(c *gin.Context) {
diskStat, _ := disk.IOCounters()
var options []string
options = append(options, "all")
for _, net := range diskStat {
options = append(options, net.Name)
}
sort.Strings(options)
helper.SuccessWithData(c, options)
helper.SuccessWithData(c, monitorService.LoadIOOptions())
}
+45
View File
@@ -0,0 +1,45 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
var req dto.MonitorVLLMSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := monitorService.LoadVLLMMonitorData(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) LoadVLLMCurrent(c *gin.Context) {
var req dto.MonitorVLLMCurrent
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := monitorService.LoadVLLMCurrent(c.Request.Context(), req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) CleanVLLMMonitor(c *gin.Context) {
var req dto.MonitorVLLMClean
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := monitorService.CleanVLLMMonitor(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+1 -1
View File
@@ -169,7 +169,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /runtimes/operate [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
func (b *BaseApi) OperateRuntime(c *gin.Context) {
var req request.RuntimeOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+2
View File
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
}
if err := loadCertAfterDecrypt(&req); err != nil {
helper.BadRequest(c, err)
return
}
if err := sshService.CreateRootCert(req); err != nil {
helper.InternalServer(c, err)
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
}
if err := loadCertAfterDecrypt(&req); err != nil {
helper.BadRequest(c, err)
return
}
if err := sshService.EditRootCert(req); err != nil {
helper.InternalServer(c, err)
+2
View File
@@ -21,6 +21,7 @@ type AlertBase struct {
}
type PushAlert struct {
Result string `json:"result,omitempty"`
TaskName string `json:"taskName"`
AlertType string `json:"alertType"`
EntryID uint `json:"entryID"`
@@ -53,6 +54,7 @@ type AlertDTO struct {
Method string `json:"method"`
Title string `json:"title"`
Project string `json:"project"`
TaskName string `json:"taskName,omitempty"`
Status string `json:"status"`
SendCount uint `json:"sendCount"`
AdvancedParams string `json:"advancedParams"`
@@ -0,0 +1,17 @@
package dto
type NetworkCleanupReport struct {
Deleted []NetworkCleanupItem `json:"deleted"`
Skipped []NetworkCleanupItem `json:"skipped"`
Failed []NetworkCleanupItem `json:"failed"`
}
type NetworkCleanupItem struct {
ID string `json:"id"`
Name string `json:"name"`
Reason string `json:"reason,omitempty"`
}
type NetworkCleanupTask struct {
TaskID string `json:"taskID"`
}
+10 -7
View File
@@ -51,9 +51,10 @@ type CronjobOperate struct {
Secret string `json:"secret"`
Args string `json:"args"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
CleanLogConfig
}
@@ -126,7 +127,8 @@ type CronjobInfo struct {
Secret string `json:"secret"`
Args string `json:"args"`
AlertCount uint `json:"alertCount"`
AlertCount uint `json:"alertCount"`
AlertTriggerMode string `json:"alertTriggerMode"`
}
type CronjobImport struct {
@@ -169,9 +171,10 @@ type CronjobTrans struct {
SourceAccounts []string `json:"sourceAccounts"`
DownloadAccount string `json:"downloadAccount"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
}
type TransHelper struct {
Name string `json:"name"`
+87 -18
View File
@@ -1,6 +1,10 @@
package dto
import "time"
import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
type MonitorSearch struct {
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
@@ -26,24 +30,33 @@ type Process struct {
}
type MonitorSetting struct {
MonitorStatus string `json:"monitorStatus"`
MonitorStoreDays string `json:"monitorStoreDays"`
MonitorInterval string `json:"monitorInterval"`
DefaultNetwork string `json:"defaultNetwork"`
DefaultIO string `json:"defaultIO"`
GPUMonitorStatus string `json:"gpuMonitorStatus"`
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
GPUMonitorInterval string `json:"gpuMonitorInterval"`
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
MonitorStatus string `json:"monitorStatus"`
MonitorStoreDays string `json:"monitorStoreDays"`
MonitorInterval string `json:"monitorInterval"`
DefaultNetwork string `json:"defaultNetwork"`
DefaultIO string `json:"defaultIO"`
}
type MonitorSettingUpdate struct {
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"`
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval GPUMonitorStatus GPUMonitorStoreDays GPUMonitorInterval VLLMMonitorStatus VLLMMonitorStoreDays VLLMMonitorInterval DefaultNetwork DefaultIO"`
Value string `json:"value"`
}
type MonitorGPUOptions struct {
Supported bool `json:"supported"`
GPUType string `json:"gpuType"`
ChartHide []GPUChartHide `json:"chartHide"`
Options []string `json:"options"`
}
type GPUChartHide struct {
DeviceID string `json:"deviceID"`
Legacy bool `json:"legacy"`
ProductName string `json:"productName"`
Type string `json:"type"`
Process bool `json:"process"`
@@ -55,23 +68,54 @@ type GPUChartHide struct {
Speed bool `json:"speed"`
}
type MonitorGPUSearch struct {
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
DeviceID string `json:"deviceID"`
Legacy bool `json:"legacy"`
ProductName string `json:"productName"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
}
type MonitorGPUData struct {
Date []time.Time `json:"date"`
GPUValue []float64 `json:"gpuValue"`
TemperatureValue []float64 `json:"temperatureValue"`
PowerTotal []float64 `json:"powerTotal"`
PowerUsed []float64 `json:"powerUsed"`
PowerPercent []float64 `json:"powerPercent"`
MemoryTotal []float64 `json:"memoryTotal"`
MemoryUsed []float64 `json:"memoryUsed"`
MemoryPercent []float64 `json:"memoryPercent"`
SpeedValue []int `json:"speedValue"`
MemoryActivity []*float64 `json:"memoryActivity"`
EncoderUtil []*float64 `json:"encoderUtil"`
DecoderUtil []*float64 `json:"decoderUtil"`
JPEGUtil []*float64 `json:"jpegUtil"`
OFAUtil []*float64 `json:"ofaUtil"`
MediaUtil []*float64 `json:"mediaUtil"`
ComputeUtil []*float64 `json:"computeUtil"`
CopyUtil []*float64 `json:"copyUtil"`
HotspotTemperature []*float64 `json:"hotspotTemperature"`
FanRPM []*float64 `json:"fanRPM"`
AICPUUtil []*float64 `json:"aiCPUUtil"`
CtrlCPUUtil []*float64 `json:"ctrlCPUUtil"`
DDRUsed []*float64 `json:"ddrUsed"`
DDRTotal []*float64 `json:"ddrTotal"`
HBMUsed []*float64 `json:"hbmUsed"`
HBMTotal []*float64 `json:"hbmTotal"`
DDRBandwidth []*float64 `json:"ddrBandwidth"`
HBMBandwidth []*float64 `json:"hbmBandwidth"`
MemoryBandwidth []*float64 `json:"memoryBandwidth"`
MediaFrequency []*float64 `json:"mediaFrequency"`
HugepagesUsed []*float64 `json:"hugepagesUsed"`
HugepagesTotal []*float64 `json:"hugepagesTotal"`
ProcessCount []int `json:"processCount"`
BucketSeconds int64 `json:"bucketSeconds"`
SampleCount int64 `json:"sampleCount"`
MemoryTemperatureValue []*float64 `json:"memoryTemperatureValue"`
FrequencyValue []*float64 `json:"frequencyValue"`
MemoryFrequencyValue []*float64 `json:"memoryFrequencyValue"`
Date []time.Time `json:"date"`
GPUValue []*float64 `json:"gpuValue"`
TemperatureValue []*float64 `json:"temperatureValue"`
PowerTotal []*float64 `json:"powerTotal"`
PowerUsed []*float64 `json:"powerUsed"`
PowerPercent []*float64 `json:"powerPercent"`
MemoryTotal []*float64 `json:"memoryTotal"`
MemoryUsed []*float64 `json:"memoryUsed"`
MemoryPercent []*float64 `json:"memoryPercent"`
SpeedValue []*float64 `json:"speedValue"`
ProcessCount []*float64 `json:"processCount"`
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
}
@@ -81,3 +125,28 @@ type GPUProcess struct {
ProcessName string `json:"processName"`
UsedMemory string `json:"usedMemory"`
}
type MonitorVLLMSearch struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
StartTime time.Time `json:"startTime" validate:"required"`
EndTime time.Time `json:"endTime" validate:"required"`
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
}
type MonitorVLLMData struct {
SampleCount int64 `json:"sampleCount"`
BucketSeconds int64 `json:"bucketSeconds"`
Points []model.MonitorVLLM `json:"points"`
}
type MonitorVLLMCurrent struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorVLLMClean struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorClean struct {
Type string `json:"type" validate:"required,oneof=host gpu"`
}
+1 -1
View File
@@ -25,7 +25,7 @@ type RootCertOperate struct {
ID uint `json:"id"`
Name string `json:"name"`
Mode string `json:"mode"`
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
EncryptionMode string `json:"encryptionMode"`
PassPhrase string `json:"passPhrase"`
PublicKey string `json:"publicKey"`
PrivateKey string `json:"privateKey"`
+75 -9
View File
@@ -1,5 +1,7 @@
package model
import "time"
type MonitorBase struct {
BaseModel
Cpu float64 `json:"cpu"`
@@ -33,14 +35,78 @@ type MonitorNetwork struct {
}
type MonitorGPU struct {
MemoryUtil *float64 `json:"memoryUtil"`
MemoryActivity *float64 `json:"memoryActivity"`
EncoderUtil *float64 `json:"encoderUtil"`
DecoderUtil *float64 `json:"decoderUtil"`
JPEGUtil *float64 `json:"jpegUtil"`
OFAUtil *float64 `json:"ofaUtil"`
MediaUtil *float64 `json:"mediaUtil"`
ComputeUtil *float64 `json:"computeUtil"`
CopyUtil *float64 `json:"copyUtil"`
HotspotTemperature *float64 `json:"hotspotTemperature"`
FanRPM *float64 `json:"fanRPM"`
AICPUUtil *float64 `json:"aiCPUUtil"`
CtrlCPUUtil *float64 `json:"ctrlCPUUtil"`
DDRUsed *float64 `json:"ddrUsed"`
DDRTotal *float64 `json:"ddrTotal"`
HBMUsed *float64 `json:"hbmUsed"`
HBMTotal *float64 `json:"hbmTotal"`
DDRBandwidth *float64 `json:"ddrBandwidth"`
HBMBandwidth *float64 `json:"hbmBandwidth"`
MemoryBandwidth *float64 `json:"memoryBandwidth"`
MediaFrequency *float64 `json:"mediaFrequency"`
HugepagesUsed *float64 `json:"hugepagesUsed"`
HugepagesTotal *float64 `json:"hugepagesTotal"`
MemoryTemperature *float64 `json:"memoryTemperature"`
DeviceID string `json:"deviceID"`
DeviceType string `json:"deviceType"`
ProcessStatus string `json:"processStatus"`
Frequency *float64 `json:"frequency"`
MemoryFrequency *float64 `json:"memoryFrequency"`
IntervalSeconds int `json:"intervalSeconds"`
BaseModel
ProductName string `json:"productName"`
GPUUtil float64 `json:"gpuUtil"`
Temperature float64 `json:"temperature"`
PowerDraw float64 `json:"powerDraw"`
MaxPowerLimit float64 `json:"maxPowerLimit"`
MemUsed float64 `json:"memUsed"`
MemTotal float64 `json:"memTotal"`
FanSpeed int `json:"fanSpeed"`
Processes string `json:"processes"`
ProductName string `json:"productName"`
GPUUtil *float64 `json:"gpuUtil"`
Temperature *float64 `json:"temperature"`
PowerDraw *float64 `json:"powerDraw"`
MaxPowerLimit *float64 `json:"maxPowerLimit"`
MemUsed *float64 `json:"memUsed"`
MemTotal *float64 `json:"memTotal"`
FanSpeed *float64 `json:"fanSpeed"`
Processes string `json:"processes"`
}
type MonitorVLLM struct {
ID uint `json:"-" gorm:"primarykey;autoIncrement"`
CreatedAt time.Time `json:"createdAt"`
AppInstallID uint `json:"appInstallID"`
Status string `json:"status"`
RawMetrics string `json:"-"`
HistogramDeltas string `json:"-"`
Running *float64 `json:"running"`
Waiting *float64 `json:"waiting"`
CacheUsage *float64 `json:"cacheUsage"`
PromptThroughput *float64 `json:"promptThroughput"`
GenerationThroughput *float64 `json:"generationThroughput"`
RequestThroughput *float64 `json:"requestThroughput"`
TimeToFirstToken *float64 `json:"timeToFirstToken"`
TimePerOutputToken *float64 `json:"timePerOutputToken"`
RequestLatency *float64 `json:"requestLatency"`
PrefillTime *float64 `json:"prefillTime"`
DecodeTime *float64 `json:"decodeTime"`
TimeToFirstTokenP50 *float64 `json:"timeToFirstTokenP50"`
TimeToFirstTokenP90 *float64 `json:"timeToFirstTokenP90"`
TimeToFirstTokenP95 *float64 `json:"timeToFirstTokenP95"`
TimeToFirstTokenP99 *float64 `json:"timeToFirstTokenP99"`
TimePerOutputTokenP50 *float64 `json:"timePerOutputTokenP50"`
TimePerOutputTokenP90 *float64 `json:"timePerOutputTokenP90"`
TimePerOutputTokenP95 *float64 `json:"timePerOutputTokenP95"`
TimePerOutputTokenP99 *float64 `json:"timePerOutputTokenP99"`
RequestLatencyP50 *float64 `json:"requestLatencyP50"`
RequestLatencyP90 *float64 `json:"requestLatencyP90"`
RequestLatencyP95 *float64 `json:"requestLatencyP95"`
RequestLatencyP99 *float64 `json:"requestLatencyP99"`
}
+103 -13
View File
@@ -1,6 +1,8 @@
package repo
import (
"fmt"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -10,11 +12,25 @@ import (
type MonitorRepo struct{}
type GPUHistoryPoint struct {
model.MonitorGPU
Bucket int64
PowerPercent *float64
MemoryPercent *float64
ProcessCount *float64
}
type IMonitorRepo interface {
CleanHost() error
CleanGPU() error
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
GetGPU(opts ...DBOption) ([]model.MonitorGPU, error)
CountGPU(opts ...DBOption) (int64, error)
GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error)
GetGPUDevices() ([]model.MonitorGPU, error)
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
GetIONames() ([]string, error)
GetNetworkNames() ([]string, error)
CreateMonitorBase(model model.MonitorBase) error
BatchCreateMonitorGPU(list []model.MonitorGPU) error
@@ -25,13 +41,26 @@ type IMonitorRepo interface {
DelMonitorIO(timeForDelete time.Time) error
DelMonitorNet(timeForDelete time.Time) error
WithByProductName(name string) DBOption
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
}
func NewIMonitorRepo() IMonitorRepo {
return &MonitorRepo{}
}
func (s *MonitorRepo) CleanHost() error {
for _, item := range []interface{}{&model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}} {
if err := global.MonitorDB.Where("1 = 1").Delete(item).Error; err != nil {
return err
}
}
return nil
}
func (s *MonitorRepo) CleanGPU() error {
return global.GPUMonitorDB.Where("1 = 1").Delete(&model.MonitorGPU{}).Error
}
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
var data []model.MonitorBase
db := global.MonitorDB
@@ -59,14 +88,16 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro
err := db.Find(&data).Error
return data, err
}
func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) {
var data []model.MonitorGPU
db := global.GPUMonitorDB
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&data).Error
return data, err
func (u *MonitorRepo) GetIONames() ([]string, error) {
var names []string
err := global.MonitorDB.Model(&model.MonitorIO{}).Distinct().Pluck("name", &names).Error
return names, err
}
func (u *MonitorRepo) GetNetworkNames() ([]string, error) {
var names []string
err := global.MonitorDB.Model(&model.MonitorNetwork{}).Distinct().Pluck("name", &names).Error
return names, err
}
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
@@ -97,8 +128,67 @@ func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error {
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
}
func (s *MonitorRepo) WithByProductName(name string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("product_name = ?", name)
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
var data []model.MonitorGPU
err := global.GPUMonitorDB.Model(&model.MonitorGPU{}).Select("device_id, product_name, device_type").Group("device_id, product_name, device_type").Order("product_name, device_id").Find(&data).Error
return data, err
}
func (u *MonitorRepo) WithByGPUDevice(deviceID, name string, legacy bool) DBOption {
return func(db *gorm.DB) *gorm.DB {
if deviceID != "" {
return db.Where("device_id = ?", deviceID)
}
db = db.Where("product_name = ?", name)
if legacy {
db = db.Where("device_id IS NULL OR device_id = ''")
}
return db
}
}
func (u *MonitorRepo) CountGPU(opts ...DBOption) (int64, error) {
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
for _, opt := range opts {
db = opt(db)
}
var count int64
err := db.Count(&count).Error
return count, err
}
func (u *MonitorRepo) GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error) {
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
for _, opt := range opts {
db = opt(db)
}
expressions := []string{
"CASE WHEN max_power_limit > 0 THEN 100.0 * power_draw / max_power_limit END",
"CASE WHEN mem_total > 0 AND mem_used IS NOT NULL THEN 100.0 * mem_used / mem_total ELSE memory_util END",
"CASE WHEN (process_status = 'ok' OR process_status IS NULL OR process_status = '') AND json_valid(processes) THEN CASE WHEN json_type(processes) = 'array' THEN json_array_length(processes) END END",
}
aliases := []string{"power_percent", "memory_percent", "process_count"}
columns := []string{"*"}
if bucketSeconds > 0 {
operation := "AVG"
if aggregation == "max" {
operation = "MAX"
}
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), bucketSeconds)}
for _, column := range []string{"memory_activity", "encoder_util", "decoder_util", "jpeg_util", "ofa_util", "media_util", "compute_util", "copy_util", "hotspot_temperature", "fan_rpm", "ai_cpu_util", "ctrl_cpu_util", "ddr_used", "ddr_total", "hbm_used", "hbm_total", "ddr_bandwidth", "hbm_bandwidth", "memory_bandwidth", "media_frequency", "hugepages_used", "hugepages_total", "gpu_util", "temperature", "memory_temperature", "power_draw", "max_power_limit", "mem_used", "mem_total", "frequency", "memory_frequency", "fan_speed"} {
columns = append(columns, operation+"("+column+") AS "+column)
}
for i := range expressions {
expressions[i] = operation + "(" + expressions[i] + ")"
}
db = db.Group("bucket").Order("bucket ASC")
} else {
db = db.Order("created_at ASC, id ASC")
}
for i, expression := range expressions {
columns = append(columns, expression+" AS "+aliases[i])
}
var data []GPUHistoryPoint
err := db.Select(strings.Join(columns, ", ")).Scan(&data).Error
return data, err
}
+78
View File
@@ -0,0 +1,78 @@
package repo
import (
"errors"
"fmt"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type VLLMMonitorRepo struct{}
type VLLMHistoryPoint struct {
model.MonitorVLLM
Bucket int64
HistogramSamples string
}
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
return global.VLLMMonitorDB.Create(point).Error
}
func (r *VLLMMonitorRepo) Latest(id uint) (model.MonitorVLLM, error) {
var point model.MonitorVLLM
db := global.VLLMMonitorDB.Where("app_install_id = ?", id)
err := db.Order("created_at DESC, id DESC").First(&point).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return point, nil
}
return point, err
}
func (r *VLLMMonitorRepo) CleanTarget(id uint) error {
return global.VLLMMonitorDB.Where("app_install_id = ?", id).Delete(&model.MonitorVLLM{}).Error
}
func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
}
func (r *VLLMMonitorRepo) Count(id uint, start, end time.Time) (int64, error) {
var count int64
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
err := db.Count(&count).Error
return count, err
}
func (r *VLLMMonitorRepo) History(id uint, start, end time.Time, seconds int64, aggregation string) ([]VLLMHistoryPoint, error) {
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
metrics := []string{"running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99"}
var columns []string
if seconds > 0 {
operation := "AVG"
if aggregation == "max" {
operation = "MAX"
}
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
for _, column := range metrics {
if aggregation != "max" && (strings.HasPrefix(column, "time_to_first_token_p") || strings.HasPrefix(column, "time_per_output_token_p") || strings.HasPrefix(column, "request_latency_p")) {
continue
}
columns = append(columns, operation+"("+column+") AS "+column)
}
if aggregation != "max" {
columns = append(columns, "json_group_array(json(NULLIF(histogram_deltas, ''))) AS histogram_samples")
}
db = db.Group("bucket").Order("bucket ASC")
} else {
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
db = db.Order("created_at ASC, id ASC")
}
var points []VLLMHistoryPoint
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
return points, err
}
+98
View File
@@ -17,6 +17,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -109,7 +110,38 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
return 0, nil, err
}
cronjobProjects := make(map[string]uint)
var cronjobIDs []uint
for _, item := range alerts {
if alertUtil.GetCronJobType(item.Type) != "cronJob" {
continue
}
if _, exists := cronjobProjects[item.Project]; exists {
continue
}
id, parseErr := strconv.ParseUint(item.Project, 10, strconv.IntSize)
if parseErr != nil || id == 0 {
continue
}
cronjobProjects[item.Project] = uint(id)
cronjobIDs = append(cronjobIDs, uint(id))
}
cronjobsByID := make(map[uint]model.Cronjob)
if len(cronjobIDs) > 0 {
cronjobs, err := cronjobRepo.List(repo.WithByIDs(cronjobIDs))
if err != nil {
return 0, nil, err
}
for _, cronjob := range cronjobs {
cronjobsByID[cronjob.ID] = cronjob
}
}
for _, item := range alerts {
var taskName string
if cronjob, exists := cronjobsByID[cronjobProjects[item.Project]]; exists && cronjob.Type == item.Type {
taskName = cronjob.Name
}
result = append(result, dto.AlertDTO{
ID: item.ID,
@@ -119,6 +151,7 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
Method: item.Method,
Title: item.Title,
Project: item.Project,
TaskName: taskName,
Status: item.Status,
SendCount: item.SendCount,
AdvancedParams: item.AdvancedParams,
@@ -190,6 +223,16 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
return err
}
} else {
advanced, err := prepareCronJobAlertParams(create.Type, "", create.AdvancedParams)
if err != nil {
return err
}
create.AdvancedParams = advanced
if create.Status != constant.AlertDisable {
if err := a.validateCronJobAlertChannels(create.Type, advanced, create.Method); err != nil {
return err
}
}
alertInfo.Status = constant.AlertEnable
if err := copier.Copy(&alertInfo, &create); err != nil {
return buserr.WithErr("ErrStructTransform", err)
@@ -207,11 +250,24 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
}
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
if alertUtil.GetCronJobType(req.Type) == "cronJob" {
previous, err := alertRepo.Get(repo.WithByID(req.ID))
if err != nil {
return err
}
req.AdvancedParams, err = prepareCronJobAlertParams(req.Type, previous.AdvancedParams, req.AdvancedParams)
if err != nil {
return err
}
}
methodTypes, err := a.validateAlertMethodReferences(req.Method)
if err != nil {
return err
}
if req.Status != constant.AlertDisable {
if err := a.validateCronJobAlertChannels(req.Type, req.AdvancedParams, req.Method); err != nil {
return err
}
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
@@ -278,6 +334,9 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
return err
}
if status == constant.AlertEnable {
if err := a.validateCronJobAlertChannels(alertInfo.Type, alertInfo.AdvancedParams, alertInfo.Method); err != nil {
return err
}
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
@@ -1021,6 +1080,23 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
alertRepo.WithByType(updateAlert.Type),
alertRepo.WithByProject(updateAlert.Project),
)
advanced, err := prepareCronJobAlertParams(updateAlert.Type, alertInfo.AdvancedParams, updateAlert.AdvancedParams)
if err != nil {
return err
}
updateAlert.AdvancedParams = advanced
if alertUtil.GetCronJobType(updateAlert.Type) == "cronJob" {
upMap["advanced_params"] = advanced
}
if newStatus == constant.AlertEnable {
method := updateAlert.Method
if method == "" {
method = alertInfo.Method
}
if err := a.validateCronJobAlertChannels(updateAlert.Type, advanced, method); err != nil {
return err
}
}
if alertInfo.ID > 0 {
shouldUpdate := false
@@ -1034,6 +1110,9 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
shouldUpdate = true
}
if val, ok := upMap["advanced_params"]; ok && val != alertInfo.AdvancedParams {
shouldUpdate = true
}
if shouldUpdate {
if err := alertRepo.Update(
@@ -1055,3 +1134,22 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
return nil
}
func prepareCronJobAlertParams(alertType, previous, incoming string) (string, error) {
if alertUtil.GetCronJobType(alertType) != "cronJob" {
return incoming, nil
}
return alertUtil.MergeCronJobAlertParams(previous, incoming)
}
func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method string) error {
if alertUtil.GetCronJobType(alertType) != "cronJob" {
return nil
}
mode, err := alertUtil.CronJobAlertTriggerMode(advanced)
if err != nil || mode != alertUtil.CronJobAlertSuccess {
return err
}
_, err = a.validateAlertMethodReferences(method)
return err
}
+18 -1
View File
@@ -530,11 +530,15 @@ func loadSSHLogin(alert dto.AlertDTO) {
if err != nil {
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
}
interfaceAddrs, err := net.InterfaceAddrs()
if err != nil {
global.LOG.Warnf("Failed to load local IP addresses for ssh login alert: %v", err)
}
count, records := summarizeSSHLoginHistories(
histories,
now,
failedWindow,
strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"),
sshSuccessLoginWhitelist(alert.AdvancedParams, interfaceAddrs),
)
isAlert := count >= int(alert.Count)
if isAlert {
@@ -570,6 +574,19 @@ func loadSSHLogin(alert dto.AlertDTO) {
}
}
func sshSuccessLoginWhitelist(configured string, interfaceAddrs []net.Addr) []string {
whitelist := strings.Split(strings.TrimSpace(configured), "\n")
whitelist = append(whitelist, "127.0.0.0/8", "::1")
for _, addr := range interfaceAddrs {
ipNet, ok := addr.(*net.IPNet)
if !ok || ipNet.IP == nil || ipNet.IP.IsUnspecified() {
continue
}
whitelist = append(whitelist, ipNet.IP.String())
}
return whitelist
}
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
filtered := make([]model.LoginLog, 0, len(records))
for _, record := range records {
+76 -34
View File
@@ -58,10 +58,10 @@ type ContainerService struct{}
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error)
Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error)
List() []dto.ContainerOptions
ListByImage(imageName string) []dto.ContainerOptions
LoadStatus() (dto.ContainerStatus, error)
LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error)
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
ListNetwork() ([]dto.Options, error)
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
@@ -80,7 +80,7 @@ type IContainerService interface {
ContainerUpdate(req dto.ContainerOperate) error
ContainerUpgrade(req dto.ContainerUpgrade) error
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
ContainerListStats() ([]dto.ContainerListStats, error)
ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error)
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
LoadResourceLimit() (*dto.ResourceLimit, error)
ContainerRename(req dto.ContainerRename) error
@@ -92,6 +92,7 @@ type IContainerService interface {
Inspect(req dto.InspectReq) (string, error)
DeleteNetwork(req dto.BatchDelete) error
CleanNetworks() (*dto.NetworkCleanupTask, error)
CreateNetwork(req dto.NetworkCreate) error
DeleteVolume(req dto.BatchDelete) error
CreateVolume(req dto.VolumeCreate) error
@@ -112,7 +113,7 @@ func NewIContainerService() IContainerService {
return &ContainerService{}
}
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) {
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
@@ -123,7 +124,7 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
options.Filters = filters.NewArgs()
options.Filters.Add("label", req.Filters)
}
containers, err := client.ContainerList(context.Background(), options)
containers, err := client.ContainerList(ctx, options)
if err != nil {
return 0, nil, err
}
@@ -206,27 +207,30 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions
return options
}
func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
func (u *ContainerService) LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) {
var data dto.ContainerStatus
client, err := docker.NewDockerClient()
if err != nil {
return data, err
}
defer client.Close()
c := context.Background()
images, _ := client.ImageList(c, image.ListOptions{All: true})
data.ImageCount = len(images)
repo, _ := imageRepoRepo.List()
data.RepoCount = len(repo)
templates, _ := composeRepo.List()
data.ComposeTemplateCount = len(templates)
networks, _ := client.NetworkList(c, network.ListOptions{})
data.NetworkCount = len(networks)
volumes, _ := client.VolumeList(c, volume.ListOptions{})
data.VolumeCount = len(volumes.Volumes)
data.ComposeCount = loadComposeCount(client)
containers, _ := client.ContainerList(c, container.ListOptions{All: true})
if !containersOnly {
images, _ := client.ImageList(ctx, image.ListOptions{All: true})
data.ImageCount = len(images)
repo, _ := imageRepoRepo.List()
data.RepoCount = len(repo)
templates, _ := composeRepo.List()
data.ComposeTemplateCount = len(templates)
networks, _ := client.NetworkList(ctx, network.ListOptions{})
data.NetworkCount = len(networks)
volumes, _ := client.VolumeList(ctx, volume.ListOptions{})
data.VolumeCount = len(volumes.Volumes)
data.ComposeCount = loadComposeCount(ctx, client)
}
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return data, err
}
data.ContainerCount = len(containers)
for _, item := range containers {
switch item.State {
@@ -292,27 +296,65 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
}
return data, nil
}
func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) {
func (u *ContainerService) ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer client.Close()
list, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
options := container.ListOptions{All: true}
if ids != nil {
if len(ids) == 0 {
return []dto.ContainerListStats{}, nil
}
options.Filters = filters.NewArgs()
for _, id := range ids {
options.Filters.Add("id", id)
}
}
list, err := client.ContainerList(ctx, options)
if err != nil {
return nil, err
}
return collectContainerStats(ctx, list, func(ctx context.Context, id string) dto.ContainerListStats {
return loadCpuAndMem(ctx, client, id)
}), nil
}
// A fixed worker pool bounds Docker stats requests, including for legacy callers
// that request all containers. Stopped containers do not need a stats sample.
func collectContainerStats(ctx context.Context, list []container.Summary, load func(context.Context, string) dto.ContainerListStats) []dto.ContainerListStats {
datas := make([]dto.ContainerListStats, len(list))
var wg sync.WaitGroup
wg.Add(len(list))
for i := 0; i < len(list); i++ {
go func(index int, item container.Summary) {
datas[index] = loadCpuAndMem(client, item.ID)
wg.Done()
}(i, list[i])
for i, item := range list {
datas[i].ContainerID = item.ID
}
jobs := make(chan int)
var wg sync.WaitGroup
for worker := 0; worker < min(8, len(list)); worker++ {
wg.Add(1)
go func() {
defer wg.Done()
for index := range jobs {
if ctx.Err() != nil || list[index].State != "running" {
continue
}
sampleCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
datas[index] = load(sampleCtx, list[index].ID)
cancel()
}
}()
}
dispatch:
for index := range list {
select {
case <-ctx.Done():
break dispatch
case jobs <- index:
}
}
close(jobs)
wg.Wait()
return datas, nil
return datas
}
func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
@@ -1715,11 +1757,11 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo
return selected
}
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
func loadCpuAndMem(ctx context.Context, client *client.Client, containerItem string) dto.ContainerListStats {
data := dto.ContainerListStats{
ContainerID: containerItem,
}
res, err := client.ContainerStats(context.Background(), containerItem, false)
res, err := client.ContainerStats(ctx, containerItem, false)
if err != nil {
return data
}
@@ -1942,11 +1984,11 @@ func transPortToStr(ports []container.Port) []string {
return docker.SimplifyPorts(ports)
}
func loadComposeCount(client *client.Client) int {
func loadComposeCount(ctx context.Context, client *client.Client) int {
options := container.ListOptions{All: true}
options.Filters = filters.NewArgs()
options.Filters.Add("label", composeProjectLabel)
list, err := client.ContainerList(context.Background(), options)
list, err := client.ContainerList(ctx, options)
if err != nil {
return 0
}
@@ -0,0 +1,71 @@
package service
import (
"fmt"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
)
var networkCleanupMu sync.Mutex
func (u *ContainerService) CleanNetworks() (*dto.NetworkCleanupTask, error) {
taskItem, err := task.NewTaskWithOps(i18n.GetMsgByKey("Network"), task.TaskClean, task.TaskScopeContainer, "", 0)
if err != nil {
return nil, err
}
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
networkCleanupMu.Lock()
defer networkCleanupMu.Unlock()
if err := t.TaskCtx.Err(); err != nil {
return err
}
cli, err := docker.NewDockerClient()
if err != nil {
return err
}
defer cli.Close()
return executeNetworkCleanup(t, cli)
}, nil)
go func() {
if err := taskItem.Execute(); err != nil {
global.LOG.Errorf("network cleanup task %s failed: %v", taskItem.TaskID, err)
}
}()
return &dto.NetworkCleanupTask{TaskID: taskItem.TaskID}, nil
}
func executeNetworkCleanup(t *task.Task, cli docker.NetworkCleanupClient) error {
t.Log(i18n.GetMsgByKey("PruneStart"))
report, err := docker.CleanUnusedNetworks(t.TaskCtx, cli, func(status string, item dto.NetworkCleanupItem) {
key := "NetworkCleanupDeleted"
if status == "skipped" || status == "failed" {
key = map[string]string{
"protected": "NetworkCleanupProtected",
"container_connected": "NetworkCleanupConnected",
"network_in_use": "NetworkCleanupConnected",
"unsupported_network": "NetworkCleanupUnsupported",
"already_removed": "NetworkCleanupGone",
"inspect_failed": "NetworkCleanupInspectFailed",
"remove_failed": "NetworkCleanupRemoveFailed",
}[item.Reason]
}
t.Log(i18n.GetMsgWithMap(key, map[string]interface{}{"name": item.Name, "id": item.ID}))
})
if report != nil {
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
"deleted": len(report.Deleted), "skipped": len(report.Skipped), "failed": len(report.Failed),
}))
}
if err != nil {
return err
}
if len(report.Failed) > 0 {
return fmt.Errorf("%s", i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
}
return nil
}
@@ -0,0 +1,113 @@
package service
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
type networkTaskClient struct{ fail bool }
func (f networkTaskClient) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
return []network.Inspect{{ID: "reserved", Name: "1panel-network", Scope: "local"}, {ID: "connected", Name: "busy", Scope: "local"}, {ID: "unused", Name: "free", Scope: "local"}, {ID: "race", Name: "race", Scope: "local"}}, nil
}
func (f networkTaskClient) ContainerList(context.Context, container.ListOptions) ([]container.Summary, error) {
return nil, nil
}
func (f networkTaskClient) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
n := network.Inspect{}
if id == "connected" {
n.Containers = map[string]network.EndpointResource{"container-id": {}}
}
return n, nil
}
func (f networkTaskClient) NetworkRemove(_ context.Context, id string) error {
if id == "race" {
return errdefs.Conflict(errors.New("has active endpoints"))
}
if id != "unused" {
return errors.New("unexpected removal")
}
if f.fail {
return errors.New("remove failed")
}
return nil
}
func TestNetworkCleanupTaskPersistsLogsAndStatus(t *testing.T) {
oldDB, oldTaskDB, oldDir, oldI18n := global.DB, global.TaskDB, global.Dir, global.I18n
t.Cleanup(func() { global.DB = oldDB; global.TaskDB = oldTaskDB; global.Dir = oldDir; global.I18n = oldI18n })
dir := t.TempDir()
db, err := gorm.Open(sqlite.Open(filepath.Join(dir, "tasks.db")), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
defer sqlDB.Close()
if err := db.AutoMigrate(&model.Task{}); err != nil {
t.Fatal(err)
}
global.DB = nil
global.TaskDB = db
global.Dir.TaskDir = dir
i18n.Init()
for _, fail := range []bool{false, true} {
name := "success"
wantStatus := constant.StatusSuccess
if fail {
name = "partial failure"
wantStatus = constant.StatusFailed
}
t.Run(name, func(t *testing.T) {
item, err := task.NewTaskWithOps("Network", task.TaskClean, task.TaskScopeContainer, "", 0)
if err != nil {
t.Fatal(err)
}
item.AddSubTask("Clean", func(t *task.Task) error { return executeNetworkCleanup(t, networkTaskClient{fail: fail}) }, nil)
err = item.Execute()
if (err != nil) != fail {
t.Fatalf("unexpected execution error: %v", err)
}
var saved model.Task
if err := db.First(&saved, "id = ?", item.TaskID).Error; err != nil {
t.Fatal(err)
}
if saved.Status != wantStatus {
t.Fatalf("status %s, want %s", saved.Status, wantStatus)
}
content, err := os.ReadFile(saved.LogFile)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"[busy] (connected): containers connected", "[race] (race): containers connected", "[1panel-network] (reserved): reserved network", "Network cleanup finished:", "[TASK-END]"} {
if !strings.Contains(string(content), want) {
t.Fatalf("missing %q in log: %s", want, content)
}
}
want := "Deleted network [free]"
if fail {
want = "Failed to remove network [free]"
}
if !strings.Contains(string(content), want) {
t.Fatalf("missing %q", want)
}
})
}
}
+92 -26
View File
@@ -16,6 +16,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
@@ -75,6 +76,7 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
EntryID: cronjob.ID,
}
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
if alertInfo.SendCount != 0 {
item.AlertCount = alertInfo.SendCount
} else {
@@ -98,9 +100,11 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
AlertType: cronjob.Type,
EntryID: cronjob.ID,
}
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))))
item.AlertMethod = alertInfo.Method
if alertInfo.SendCount != 0 {
item.AlertTitle = alertInfo.Title
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
if alertInfo.Status == constant.AlertEnable {
item.AlertCount = alertInfo.SendCount
} else {
item.AlertCount = 0
@@ -195,11 +199,12 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
}
}
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable))
if alertInfo.SendCount != 0 {
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))))
item.AlertTitle = alertInfo.Title
item.AlertMethod = alertInfo.Method
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
if alertInfo.Status == constant.AlertEnable {
item.AlertCount = alertInfo.SendCount
item.AlertTitle = alertInfo.Title
item.AlertMethod = alertInfo.Method
} else {
item.AlertCount = 0
}
@@ -213,6 +218,17 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
}
func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
for _, item := range req {
advanced, err := cronJobAlertAdvancedParams(item.AlertTriggerMode)
if err != nil {
return err
}
if item.AlertCount != 0 {
if err := (AlertService{}).validateCronJobAlertChannels(item.Type, advanced, item.AlertMethod); err != nil {
return err
}
}
}
for _, item := range req {
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
if cronjobItem.ID != 0 {
@@ -395,17 +411,27 @@ func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
} else {
cronjob.Status = constant.StatusDisable
}
_ = cronjobRepo.Create(&cronjob)
if item.AlertCount != 0 && item.AlertTitle != "" && item.AlertMethod != "" {
if err := cronjobRepo.Create(&cronjob); err != nil {
return err
}
if item.AlertTitle != "" && item.AlertMethod != "" {
advanced, _ := cronJobAlertAdvancedParams(item.AlertTriggerMode)
status := constant.AlertEnable
if item.AlertCount == 0 {
status = constant.AlertDisable
}
createAlert := dto.AlertCreate{
Title: item.AlertTitle,
SendCount: item.AlertCount,
Method: item.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
Title: item.AlertTitle,
SendCount: item.AlertCount,
Method: item.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: status,
AdvancedParams: advanced,
}
if err := NewIAlertService().CreateAlert(createAlert, operator); err != nil {
return err
}
_ = NewIAlertService().CreateAlert(createAlert, operator)
}
}
return nil
@@ -562,6 +588,15 @@ func (u *CronjobService) HandleOnce(id uint) error {
}
func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
if err != nil {
return err
}
if req.AlertCount != 0 {
if err := (AlertService{}).validateCronJobAlertChannels(req.Type, advanced, req.AlertMethod); err != nil {
return err
}
}
cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name))
if cronjob.ID != 0 {
return buserr.New("ErrRecordExist")
@@ -603,12 +638,13 @@ func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
}
if req.AlertCount != 0 && req.AlertTitle != "" && req.AlertMethod != "" {
createAlert := dto.AlertCreate{
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
AdvancedParams: advanced,
}
err := NewIAlertService().CreateAlert(createAlert, operator)
if err != nil {
@@ -682,6 +718,10 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
}
func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error {
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
if err != nil {
return err
}
var cronjob model.Cronjob
if err := copier.Copy(&cronjob, &req); err != nil {
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
@@ -697,6 +737,20 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
if err != nil {
return buserr.New("ErrRecordNotFound")
}
if req.AlertCount != 0 {
previous, _ := alertRepo.Get(alertRepo.WithByType(cronModel.Type), alertRepo.WithByProject(strconv.Itoa(int(id))))
merged, err := prepareCronJobAlertParams(cronModel.Type, previous.AdvancedParams, advanced)
if err != nil {
return err
}
method := req.AlertMethod
if method == "" {
method = previous.Method
}
if err := (AlertService{}).validateCronJobAlertChannels(cronModel.Type, merged, method); err != nil {
return err
}
}
upMap := make(map[string]interface{})
cronjob.EntryIDs = cronModel.EntryIDs
cronjob.Type = cronModel.Type
@@ -753,11 +807,12 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
return err
}
updateAlert := dto.AlertCreate{
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronModel.ID)),
Title: req.AlertTitle,
SendCount: req.AlertCount,
Method: req.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronModel.ID)),
AdvancedParams: advanced,
}
err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
if err != nil {
@@ -766,6 +821,17 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
return nil
}
func cronJobAlertAdvancedParams(mode string) (string, error) {
if mode == "" {
return "", nil
}
data, err := json.Marshal(map[string]string{"alertTriggerMode": mode})
if err != nil {
return "", err
}
return alertUtil.MergeCronJobAlertParams("", string(data))
}
func (u *CronjobService) UpdateStatus(id uint, status string) error {
cronjob, _ := cronjobRepo.Get(repo.WithByID(id))
if cronjob.ID == 0 {
+1
View File
@@ -412,6 +412,7 @@ func addSkipTask(source string, taskItem *task.Task) {
taskItem.Log(i18n.GetMsgByKey("NoSuchResource"))
return nil
}, nil)
taskItem.SubTasks[len(taskItem.SubTasks)-1].StepAlias = cronJobSkippedStep
}
func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
+35 -6
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -23,6 +24,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
@@ -56,10 +58,11 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
_ = taskRepo.Save(context.Background(), taskItem.Task)
}
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
handleCronJobAlert(cronjob)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
return
}
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
}()
return
}
@@ -70,19 +73,20 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
record.TaskID = ""
}
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
handleCronJobAlert(cronjob)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
return
}
go func() {
if err := taskItem.Execute(); err != nil {
taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
if len(taskItem.ID) == 0 {
storedTask, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
if len(storedTask.ID) == 0 {
record.TaskID = ""
}
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
handleCronJobAlert(cronjob)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
} else {
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
}
}()
}
@@ -482,8 +486,33 @@ func hasBackup(cronjobType string) bool {
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" || cronjobType == "cutWebsiteLog"
}
func handleCronJobAlert(cronjob *model.Cronjob) {
const cronJobSkippedStep = "cronjob-skipped"
func cronJobAlertResult(taskItem *task.Task, err error) string {
if errors.Is(err, context.Canceled) || taskItem.Task.Status == constant.StatusCanceled ||
(taskItem.TaskCtx != nil && taskItem.TaskCtx.Err() != nil) {
return ""
}
if err != nil {
return alertUtil.CronJobAlertFailed
}
if taskItem.Task.Status != constant.StatusSuccess {
return ""
}
for _, subTask := range taskItem.SubTasks {
if subTask.StepAlias != cronJobSkippedStep {
return alertUtil.CronJobAlertSuccess
}
}
return ""
}
func handleCronJobAlert(cronjob *model.Cronjob, result string) {
if result == "" {
return
}
pushAlert := dto.PushAlert{
Result: result,
TaskName: cronjob.Name,
AlertType: cronjob.Type,
EntryID: cronjob.ID,
+8 -1
View File
@@ -588,6 +588,9 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
xpuData []dto.XPUInfo
)
for _, device := range snapshot.Devices {
if device.ParentID != "" {
continue
}
switch device.Kind {
case accelerator.KindGPU:
if device.GPU == nil {
@@ -597,7 +600,11 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
if err := copier.Copy(&dataItem, device.GPU); err != nil {
continue
}
dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit
dataItem.MaxPowerLimit = device.GPU.PowerLimit
dataItem.PowerUsage = dataItem.PowerDraw
if dataItem.MaxPowerLimit != "" {
dataItem.PowerUsage += " / " + dataItem.MaxPowerLimit
}
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
gpuData = append(gpuData, dataItem)
case accelerator.KindNPU:
+5 -4
View File
@@ -32,10 +32,11 @@ var (
scriptRepo = repo.NewIScriptRepo()
cronjobRepo = repo.NewICronjobRepo()
hostRepo = repo.NewIHostRepo()
ftpRepo = repo.NewIFtpRepo()
clamRepo = repo.NewIClamRepo()
monitorRepo = repo.NewIMonitorRepo()
hostRepo = repo.NewIHostRepo()
ftpRepo = repo.NewIFtpRepo()
clamRepo = repo.NewIClamRepo()
monitorRepo = repo.NewIMonitorRepo()
vllmMonitorRepo = &repo.VLLMMonitorRepo{}
settingRepo = repo.NewISettingRepo()
forwardingRuleRepo = repo.NewIForwardingRuleRepo()
+1 -1
View File
@@ -28,7 +28,7 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/archive"
"github.com/moby/go-archive"
)
type ImageService struct{}
+437 -138
View File
@@ -33,11 +33,14 @@ import (
type MonitorService struct {
DiskIO chan ([]disk.IOCountersStat)
NetIO chan ([]net.IOCountersStat)
ctx context.Context
}
var (
monitorCancel context.CancelFunc
hostSysPath = loadHostSysPath()
monitorSettingMutex sync.Mutex
gpuMonitorMutex sync.Mutex
monitorCancel context.CancelFunc
hostSysPath = loadHostSysPath()
blockDevicePartitionCache sync.Map
)
@@ -47,8 +50,13 @@ type IMonitorService interface {
LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorData, error)
LoadSetting() (*dto.MonitorSetting, error)
UpdateSetting(key, value string) error
CleanData() error
CleanData(monitorType string) error
LoadIOOptions() []string
LoadNetworkOptions() []string
LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error)
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
LoadGPUOptions() dto.MonitorGPUOptions
LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error)
@@ -60,6 +68,7 @@ func NewIMonitorService() IMonitorService {
return &MonitorService{
DiskIO: make(chan []disk.IOCountersStat, 2),
NetIO: make(chan []net.IOCountersStat, 2),
ctx: context.Background(),
}
}
@@ -85,7 +94,7 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
base.TopCPUItems = processes
base.TopCPU = ""
}
if req.Param == "all" || req.Param == "mem" {
if req.Param == "all" || req.Param == "memory" {
var processes []dto.Process
_ = json.Unmarshal([]byte(base.TopMem), &processes)
base.TopMemItems = processes
@@ -96,7 +105,11 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
data = append(data, itemData)
}
if req.Param == "all" || req.Param == "io" {
bases, err := monitorRepo.GetIO(repo.WithByName(req.IO), repo.WithByCreatedAt(req.StartTime, req.EndTime))
ioOpts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime)}
if len(req.IO) != 0 {
ioOpts = append(ioOpts, repo.WithByName(req.IO))
}
bases, err := monitorRepo.GetIO(ioOpts...)
if err != nil {
return nil, err
}
@@ -110,7 +123,11 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
data = append(data, itemData)
}
if req.Param == "all" || req.Param == "network" {
bases, err := monitorRepo.GetNetwork(repo.WithByName(req.Network), repo.WithByCreatedAt(req.StartTime, req.EndTime))
netOpts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime)}
if len(req.Network) != 0 {
netOpts = append(netOpts, repo.WithByName(req.Network))
}
bases, err := monitorRepo.GetNetwork(netOpts...)
if err != nil {
return nil, err
}
@@ -126,117 +143,171 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
return data, nil
}
func (m *MonitorService) LoadIOOptions() []string {
optionSet := make(map[string]struct{})
if diskStat, err := disk.IOCounters(); err == nil {
for _, item := range diskStat {
optionSet[item.Name] = struct{}{}
}
}
// union with names recorded in the monitor db so removed devices stay selectable
if names, err := monitorRepo.GetIONames(); err == nil {
for _, name := range names {
optionSet[name] = struct{}{}
}
}
return sortedMonitorOptions(optionSet)
}
func (m *MonitorService) LoadNetworkOptions() []string {
optionSet := make(map[string]struct{})
if netStat, err := net.IOCounters(true); err == nil {
for _, item := range netStat {
optionSet[item.Name] = struct{}{}
}
}
if names, err := monitorRepo.GetNetworkNames(); err == nil {
for _, name := range names {
optionSet[name] = struct{}{}
}
}
return sortedMonitorOptions(optionSet)
}
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions
exist, client := accelerator.New()
if !exist {
return data
}
seen := make(map[string]bool)
snapshot, err := client.Collect(context.Background())
if err != nil {
global.LOG.Errorf("Load accelerator info failed, err: %v", err)
global.LOG.Warnf("Load accelerator options failed: %v", err)
} else {
data = loadGPUOptions(snapshot)
for _, item := range data.ChartHide {
seen[item.DeviceID] = true
}
}
data.Supported = true
devices, err := monitorRepo.GetGPUDevices()
if err != nil {
global.LOG.Warnf("Load accelerator history options failed: %v", err)
return data
}
if warning := snapshot.Warning(); warning != nil {
global.LOG.Warnf("Load accelerator info partially failed, err: %v", warning)
}
return loadGPUOptions(snapshot)
}
func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions
hasGPUOrNPU := false
hasXPU := false
for _, item := range snapshot.Devices {
if item.Kind == accelerator.KindXPU {
hasXPU = true
} else {
hasGPUOrNPU = true
for _, device := range devices {
key := device.DeviceID
if key == "" {
key = "legacy:" + device.ProductName
}
}
switch {
case hasGPUOrNPU && hasXPU:
data.GPUType = "mixed"
case hasXPU:
data.GPUType = "xpu"
case hasGPUOrNPU:
data.GPUType = "gpu"
}
sort.Slice(snapshot.Devices, func(i, j int) bool {
if snapshot.Devices[i].Kind != snapshot.Devices[j].Kind {
return snapshot.Devices[i].Kind < snapshot.Devices[j].Kind
if seen[key] {
continue
}
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor {
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor
}
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex {
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex
}
if snapshot.Devices[i].ChipIndex != snapshot.Devices[j].ChipIndex {
return snapshot.Devices[i].ChipIndex < snapshot.Devices[j].ChipIndex
}
return snapshot.Devices[i].Index < snapshot.Devices[j].Index
})
for _, item := range snapshot.Devices {
optionType := "gpu"
if item.Kind == accelerator.KindXPU {
optionType = "xpu"
}
chartHide := dto.GPUChartHide{
ProductName: item.Label,
Type: optionType,
GPU: !item.Capabilities.Utilization,
Memory: !item.Capabilities.Memory,
Power: !item.Capabilities.Power,
PowerLimit: !item.Capabilities.PowerLimit,
Temperature: !item.Capabilities.Temperature,
Speed: !item.Capabilities.FanSpeed,
}
data.ChartHide = append(data.ChartHide, chartHide)
data.Options = append(data.Options, chartHide.ProductName)
seen[key] = true
data.ChartHide = append(data.ChartHide, dto.GPUChartHide{DeviceID: device.DeviceID, ProductName: device.ProductName, Type: device.DeviceType, Legacy: device.DeviceID == ""})
data.Options = append(data.Options, device.ProductName)
}
return data
}
func (m *MonitorService) LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error) {
loc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
req.StartTime = req.StartTime.In(loc)
req.EndTime = req.EndTime.In(loc)
var data dto.MonitorGPUData
gpuList, err := monitorRepo.GetGPU(repo.WithByCreatedAt(req.StartTime, req.EndTime), monitorRepo.WithByProductName(req.ProductName))
if req.StartTime.IsZero() || req.EndTime.IsZero() || !req.EndTime.After(req.StartTime) {
return data, fmt.Errorf("invalid GPU history time range")
}
if req.DeviceID == "" && req.ProductName == "" {
return data, fmt.Errorf("GPU history requires a device")
}
if req.Aggregation != "" && req.Aggregation != "avg" && req.Aggregation != "max" {
return data, fmt.Errorf("invalid GPU history aggregation")
}
loc, err := time.LoadLocation(common.LoadTimeZoneByCmd())
if err != nil {
return data, err
}
for _, gpu := range gpuList {
data.Date = append(data.Date, gpu.CreatedAt)
data.GPUValue = append(data.GPUValue, gpu.GPUUtil)
data.TemperatureValue = append(data.TemperatureValue, gpu.Temperature)
data.PowerUsed = append(data.PowerUsed, gpu.PowerDraw)
data.PowerTotal = append(data.PowerTotal, gpu.MaxPowerLimit)
if gpu.MaxPowerLimit != 0 {
data.PowerPercent = append(data.PowerPercent, gpu.PowerDraw/gpu.MaxPowerLimit*100)
} else {
data.PowerPercent = append(data.PowerPercent, float64(0))
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
opts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime), monitorRepo.WithByGPUDevice(req.DeviceID, req.ProductName, req.Legacy)}
data.SampleCount, err = monitorRepo.CountGPU(opts...)
if err != nil || data.SampleCount == 0 {
return data, err
}
if data.SampleCount > 1200 {
seconds := req.EndTime.Unix() - req.StartTime.Unix() + 1
data.BucketSeconds = (seconds + 599) / 600
}
points, err := monitorRepo.GetGPUHistory(req.StartTime, data.BucketSeconds, req.Aggregation, opts...)
if err != nil {
return data, err
}
samples := make([]repo.GPUHistoryPoint, 0, len(points))
if data.BucketSeconds > 0 {
next := 0
for bucket := int64(0); bucket <= (req.EndTime.Unix()-req.StartTime.Unix())/data.BucketSeconds; bucket++ {
point := repo.GPUHistoryPoint{}
if next < len(points) && points[next].Bucket == bucket {
point = points[next]
next++
}
point.CreatedAt = time.Unix(req.StartTime.Unix()+bucket*data.BucketSeconds, 0).In(loc)
if bucket == 0 {
point.CreatedAt = req.StartTime
}
samples = append(samples, point)
}
data.MemoryTotal = append(data.MemoryTotal, gpu.MemTotal)
data.MemoryUsed = append(data.MemoryUsed, gpu.MemUsed)
if gpu.MemTotal != 0 {
data.MemoryPercent = append(data.MemoryPercent, gpu.MemUsed/gpu.MemTotal*100)
} else {
data.MemoryPercent = append(data.MemoryPercent, float64(0))
} else {
for i, point := range points {
if i > 0 && points[i-1].IntervalSeconds > 0 {
interval := time.Duration(points[i-1].IntervalSeconds) * time.Second
if point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
samples = append(samples, repo.GPUHistoryPoint{MonitorGPU: model.MonitorGPU{BaseModel: model.BaseModel{CreatedAt: points[i-1].CreatedAt.Add(interval)}}})
}
}
samples = append(samples, point)
}
var process []dto.GPUProcess
if err := json.Unmarshal([]byte(gpu.Processes), &process); err == nil {
data.ProcessCount = append(data.ProcessCount, len(process))
data.GPUProcesses = append(data.GPUProcesses, process)
} else {
data.ProcessCount = append(data.ProcessCount, 0)
data.GPUProcesses = append(data.GPUProcesses, []dto.GPUProcess{})
}
for _, point := range samples {
data.Date = append(data.Date, point.CreatedAt)
data.MemoryActivity = append(data.MemoryActivity, point.MemoryActivity)
data.EncoderUtil = append(data.EncoderUtil, point.EncoderUtil)
data.DecoderUtil = append(data.DecoderUtil, point.DecoderUtil)
data.JPEGUtil = append(data.JPEGUtil, point.JPEGUtil)
data.OFAUtil = append(data.OFAUtil, point.OFAUtil)
data.MediaUtil = append(data.MediaUtil, point.MediaUtil)
data.ComputeUtil = append(data.ComputeUtil, point.ComputeUtil)
data.CopyUtil = append(data.CopyUtil, point.CopyUtil)
data.HotspotTemperature = append(data.HotspotTemperature, point.HotspotTemperature)
data.FanRPM = append(data.FanRPM, point.FanRPM)
data.AICPUUtil = append(data.AICPUUtil, point.AICPUUtil)
data.CtrlCPUUtil = append(data.CtrlCPUUtil, point.CtrlCPUUtil)
data.DDRUsed = append(data.DDRUsed, point.DDRUsed)
data.DDRTotal = append(data.DDRTotal, point.DDRTotal)
data.HBMUsed = append(data.HBMUsed, point.HBMUsed)
data.HBMTotal = append(data.HBMTotal, point.HBMTotal)
data.DDRBandwidth = append(data.DDRBandwidth, point.DDRBandwidth)
data.HBMBandwidth = append(data.HBMBandwidth, point.HBMBandwidth)
data.MemoryBandwidth = append(data.MemoryBandwidth, point.MemoryBandwidth)
data.MediaFrequency = append(data.MediaFrequency, point.MediaFrequency)
data.HugepagesUsed = append(data.HugepagesUsed, point.HugepagesUsed)
data.HugepagesTotal = append(data.HugepagesTotal, point.HugepagesTotal)
data.GPUValue = append(data.GPUValue, point.GPUUtil)
data.TemperatureValue = append(data.TemperatureValue, point.Temperature)
data.MemoryTemperatureValue = append(data.MemoryTemperatureValue, point.MemoryTemperature)
data.PowerUsed = append(data.PowerUsed, point.PowerDraw)
data.PowerTotal = append(data.PowerTotal, point.MaxPowerLimit)
data.PowerPercent = append(data.PowerPercent, point.PowerPercent)
data.MemoryPercent = append(data.MemoryPercent, point.MemoryPercent)
data.MemoryTotal = append(data.MemoryTotal, point.MemTotal)
data.MemoryUsed = append(data.MemoryUsed, point.MemUsed)
data.SpeedValue = append(data.SpeedValue, point.FanSpeed)
data.FrequencyValue = append(data.FrequencyValue, point.Frequency)
data.MemoryFrequencyValue = append(data.MemoryFrequencyValue, point.MemoryFrequency)
data.ProcessCount = append(data.ProcessCount, point.ProcessCount)
var processes []dto.GPUProcess
if data.BucketSeconds == 0 && point.ProcessCount != nil {
_ = json.Unmarshal([]byte(point.Processes), &processes)
}
data.SpeedValue = append(data.SpeedValue, gpu.FanSpeed)
data.GPUProcesses = append(data.GPUProcesses, processes)
}
return data, nil
}
@@ -262,9 +333,27 @@ func (m *MonitorService) LoadSetting() (*dto.MonitorSetting, error) {
}
func (m *MonitorService) UpdateSetting(key, value string) error {
monitorSettingMutex.Lock()
defer monitorSettingMutex.Unlock()
switch key {
case "MonitorStatus", "GPUMonitorStatus", "VLLMMonitorStatus":
if value != constant.StatusEnable && value != constant.StatusDisable {
return fmt.Errorf("invalid monitoring status")
}
case "MonitorInterval", "GPUMonitorInterval", "VLLMMonitorInterval":
interval, err := strconv.Atoi(value)
if err != nil || interval < 10 || interval > 43200 {
return fmt.Errorf("monitoring interval must be between 10 and 43200 seconds")
}
case "MonitorStoreDays", "GPUMonitorStoreDays", "VLLMMonitorStoreDays":
days, err := strconv.Atoi(value)
if err != nil || days < 1 {
return fmt.Errorf("monitoring retention must be a positive integer")
}
}
switch key {
case "MonitorStatus":
if value == constant.StatusEnable && global.MonitorCronID == 0 {
if value == constant.StatusEnable && monitorCancel == nil {
interval, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval"))
if err != nil {
return err
@@ -273,8 +362,9 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
return err
}
}
if value == constant.StatusDisable && global.MonitorCronID != 0 {
if value == constant.StatusDisable && monitorCancel != nil {
monitorCancel()
monitorCancel = nil
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
global.MonitorCronID = 0
}
@@ -283,31 +373,80 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
if err != nil {
return err
}
if status.Value == constant.StatusEnable && global.MonitorCronID != 0 {
if status.Value == constant.StatusEnable && monitorCancel != nil {
if err := StartMonitor(true, value); err != nil {
return err
}
}
case "GPUMonitorStatus":
if value == constant.StatusEnable && global.GPUMonitorCronID == 0 {
interval, err := settingRepo.GetValueByKey("GPUMonitorInterval")
if err != nil {
return err
}
if err := StartGPUMonitor(interval); err != nil {
return err
}
}
if value == constant.StatusDisable && global.GPUMonitorCronID != 0 {
global.Cron.Remove(global.GPUMonitorCronID)
global.GPUMonitorCronID = 0
}
case "GPUMonitorInterval":
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
if err != nil {
return err
}
if status == constant.StatusEnable {
if err := StartGPUMonitor(value); err != nil {
return err
}
}
case "VLLMMonitorStatus":
if value == constant.StatusEnable && global.VLLMMonitorCronID == 0 {
interval, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
if err != nil {
return err
}
if err := StartVLLMMonitor(interval); err != nil {
return err
}
}
if value == constant.StatusDisable && global.VLLMMonitorCronID != 0 {
global.Cron.Remove(global.VLLMMonitorCronID)
global.VLLMMonitorCronID = 0
}
case "VLLMMonitorInterval":
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
if err != nil {
return err
}
if status == constant.StatusEnable {
if err := StartVLLMMonitor(value); err != nil {
return err
}
}
}
return settingRepo.Update(key, value)
}
func (m *MonitorService) CleanData() error {
if err := global.MonitorDB.Exec("DELETE FROM monitor_bases").Error; err != nil {
return err
func (m *MonitorService) CleanData(monitorType string) error {
switch monitorType {
case "host":
return monitorRepo.CleanHost()
case "gpu":
gpuMonitorMutex.Lock()
defer gpuMonitorMutex.Unlock()
return monitorRepo.CleanGPU()
default:
return fmt.Errorf("unsupported monitoring cleanup type: %s", monitorType)
}
if err := global.MonitorDB.Exec("DELETE FROM monitor_ios").Error; err != nil {
return err
}
if err := global.MonitorDB.Exec("DELETE FROM monitor_networks").Error; err != nil {
return err
}
_ = global.GPUMonitorDB.Exec("DELETE FROM monitor_gpus").Error
return nil
}
func (m *MonitorService) Run() {
saveAcceleratorDataToDB()
if m.ctx.Err() != nil {
return
}
var itemModel model.MonitorBase
totalPercent, _ := cpu.Percent(3*time.Second, false)
if len(totalPercent) == 1 {
@@ -353,30 +492,34 @@ func (m *MonitorService) Run() {
_ = monitorRepo.DelMonitorBase(timeForDelete)
_ = monitorRepo.DelMonitorIO(timeForDelete)
_ = monitorRepo.DelMonitorNet(timeForDelete)
_ = monitorRepo.DelMonitorGPU(timeForDelete)
}
func (m *MonitorService) loadDiskIO() {
ioStat, _ := disk.IOCounters()
ioStat, _ := disk.IOCountersWithContext(m.ctx)
var diskIOList []disk.IOCountersStat
for _, io := range ioStat {
diskIOList = append(diskIOList, io)
}
diskIOList = append(diskIOList, sumDiskIOCounters(ioStat))
m.DiskIO <- diskIOList
select {
case <-m.ctx.Done():
case m.DiskIO <- diskIOList:
}
}
func (m *MonitorService) loadNetIO() {
netStat, _ := net.IOCounters(true)
netStatAll, _ := net.IOCounters(false)
netStat, _ := net.IOCountersWithContext(m.ctx, true)
netStatAll, _ := net.IOCountersWithContext(m.ctx, false)
var netList []net.IOCountersStat
netList = append(netList, netStat...)
netList = append(netList, netStatAll...)
m.NetIO <- netList
select {
case <-m.ctx.Done():
case m.NetIO <- netList:
}
}
func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
defer close(m.DiskIO)
for {
select {
case <-ctx.Done():
@@ -426,14 +569,17 @@ func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
}
}
_ = monitorRepo.BatchCreateMonitorIO(ioList)
m.DiskIO <- ioStat2
select {
case <-ctx.Done():
return
case m.DiskIO <- ioStat2:
}
}
}
}
}
func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64) {
defer close(m.NetIO)
for {
select {
case <-ctx.Done():
@@ -463,7 +609,11 @@ func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64)
}
_ = monitorRepo.BatchCreateMonitorNet(netList)
m.NetIO <- netStat2
select {
case <-ctx.Done():
return
case m.NetIO <- netStat2:
}
}
}
}
@@ -572,27 +722,38 @@ func loadTopMem() []dto.Process {
}
func StartMonitor(removeBefore bool, interval string) error {
intervalItem, err := strconv.Atoi(interval)
if err != nil || intervalItem < 10 || intervalItem > 43200 {
return fmt.Errorf("invalid host monitoring interval: %s", interval)
}
if removeBefore {
monitorCancel()
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
}
intervalItem, err := strconv.Atoi(interval)
if err != nil {
return err
}
service := NewIMonitorService()
ctx, cancel := context.WithCancel(context.Background())
monitorCancel = cancel
service := &MonitorService{
DiskIO: make(chan []disk.IOCountersStat, 2),
NetIO: make(chan []net.IOCountersStat, 2),
ctx: ctx,
}
now := time.Now()
nextMinute := now.Truncate(time.Minute).Add(time.Minute)
time.AfterFunc(time.Until(nextMinute), func() {
timer := time.AfterFunc(time.Until(nextMinute), func() {
monitorSettingMutex.Lock()
defer monitorSettingMutex.Unlock()
if ctx.Err() != nil {
return
}
monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %ss", interval), service)
if err != nil {
return
}
global.MonitorCronID = monitorID
})
monitorCancel = func() {
cancel()
timer.Stop()
}
service.Run()
@@ -602,7 +763,97 @@ func StartMonitor(removeBefore bool, interval string) error {
return nil
}
func saveAcceleratorDataToDB() {
func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions
hasGPUOrNPU := false
hasXPU := false
for _, item := range snapshot.Devices {
if item.Kind == accelerator.KindXPU {
hasXPU = true
} else {
hasGPUOrNPU = true
}
}
switch {
case hasGPUOrNPU && hasXPU:
data.GPUType = "mixed"
case hasXPU:
data.GPUType = "xpu"
case hasGPUOrNPU:
data.GPUType = "gpu"
}
sort.Slice(snapshot.Devices, func(i, j int) bool {
if snapshot.Devices[i].Kind != snapshot.Devices[j].Kind {
return snapshot.Devices[i].Kind < snapshot.Devices[j].Kind
}
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor {
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor
}
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex {
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex
}
if snapshot.Devices[i].ChipIndex != snapshot.Devices[j].ChipIndex {
return snapshot.Devices[i].ChipIndex < snapshot.Devices[j].ChipIndex
}
return snapshot.Devices[i].Index < snapshot.Devices[j].Index
})
for _, item := range snapshot.Devices {
chartHide := dto.GPUChartHide{
DeviceID: item.ID,
ProductName: item.Label,
Type: string(item.Kind),
}
data.ChartHide = append(data.ChartHide, chartHide)
data.Options = append(data.Options, chartHide.ProductName)
}
return data
}
func StartGPUMonitor(interval string) error {
seconds, err := strconv.Atoi(interval)
if err != nil || seconds < 10 || seconds > 43200 {
return fmt.Errorf("invalid GPU monitoring interval: %s", interval)
}
service := &MonitorService{}
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveGPUData))
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
if err != nil {
return err
}
if global.GPUMonitorCronID != 0 {
global.Cron.Remove(global.GPUMonitorCronID)
}
global.GPUMonitorCronID = id
return nil
}
func (m *MonitorService) saveGPUData() {
if !gpuMonitorMutex.TryLock() {
return
}
defer gpuMonitorMutex.Unlock()
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
if err != nil {
global.LOG.Errorf("Load GPU monitoring status failed: %v", err)
return
}
if status != constant.StatusEnable {
return
}
retention, err := settingRepo.GetValueByKey("GPUMonitorStoreDays")
if err != nil {
global.LOG.Errorf("Load GPU monitoring retention failed: %v", err)
return
}
days, err := strconv.Atoi(retention)
if err != nil || days < 1 {
global.LOG.Errorf("Invalid GPU monitoring retention: %s", retention)
return
}
if err := monitorRepo.DelMonitorGPU(time.Now().AddDate(0, 0, -days)); err != nil {
global.LOG.Errorf("Clean GPU monitoring data failed: %v", err)
}
exist, client := accelerator.New()
if !exist {
return
@@ -615,27 +866,64 @@ func saveAcceleratorDataToDB() {
if warning := snapshot.Warning(); warning != nil {
global.LOG.Warnf("load accelerator monitor data partially failed, err: %v", warning)
}
intervalSeconds := 0
if setting, err := settingRepo.Get(settingRepo.WithByKey("GPUMonitorInterval")); err == nil {
intervalSeconds, _ = strconv.Atoi(setting.Value)
}
list := make([]model.MonitorGPU, 0, len(snapshot.Devices))
for _, device := range snapshot.Devices {
list = append(list, newMonitorGPU(device))
item := newMonitorGPU(device)
item.CreatedAt = snapshot.Info.CollectedAt
item.IntervalSeconds = intervalSeconds
list = append(list, item)
}
if err := repo.NewIMonitorRepo().BatchCreateMonitorGPU(list); err != nil {
if err := monitorRepo.BatchCreateMonitorGPU(list); err != nil {
global.LOG.Errorf("batch create accelerator monitor data failed, err: %v", err)
}
}
func newMonitorGPU(device accelerator.Device) model.MonitorGPU {
item := model.MonitorGPU{
ProductName: device.Label,
GPUUtil: device.Metrics.Utilization.ValueOrZero(),
Temperature: device.Metrics.Temperature.ValueOrZero(),
PowerDraw: device.Metrics.Power.ValueOrZero(),
MaxPowerLimit: device.Metrics.PowerLimit.ValueOrZero(),
MemUsed: device.Metrics.MemoryUsed.ValueOrZero(),
MemTotal: device.Metrics.MemoryTotal.ValueOrZero(),
FanSpeed: int(device.Metrics.FanSpeed.ValueOrZero()),
MemoryUtil: device.Metrics.MemoryUtil.Value,
MemoryActivity: device.Metrics.MemoryActivity.Value,
EncoderUtil: device.Metrics.EncoderUtil.Value,
DecoderUtil: device.Metrics.DecoderUtil.Value,
JPEGUtil: device.Metrics.JPEGUtil.Value,
OFAUtil: device.Metrics.OFAUtil.Value,
MediaUtil: device.Metrics.MediaUtil.Value,
ComputeUtil: device.Metrics.ComputeUtil.Value,
CopyUtil: device.Metrics.CopyUtil.Value,
HotspotTemperature: device.Metrics.HotspotTemperature.Value,
FanRPM: device.Metrics.FanRPM.Value,
AICPUUtil: device.Metrics.AICPUUtil.Value,
CtrlCPUUtil: device.Metrics.CtrlCPUUtil.Value,
DDRUsed: device.Metrics.DDRUsed.Value,
DDRTotal: device.Metrics.DDRTotal.Value,
HBMUsed: device.Metrics.HBMUsed.Value,
HBMTotal: device.Metrics.HBMTotal.Value,
DDRBandwidth: device.Metrics.DDRBandwidth.Value,
HBMBandwidth: device.Metrics.HBMBandwidth.Value,
MemoryBandwidth: device.Metrics.MemoryBandwidth.Value,
MediaFrequency: device.Metrics.MediaFrequency.Value,
HugepagesUsed: device.Metrics.HugepagesUsed.Value,
HugepagesTotal: device.Metrics.HugepagesTotal.Value,
ProductName: device.Label,
DeviceID: device.ID,
DeviceType: string(device.Kind),
ProcessStatus: device.ProcessStatus,
Frequency: device.Metrics.Frequency.Value,
MemoryFrequency: device.Metrics.MemoryFrequency.Value,
MemoryTemperature: device.Metrics.MemoryTemperature.Value,
GPUUtil: device.Metrics.Utilization.Value,
Temperature: device.Metrics.Temperature.Value,
PowerDraw: device.Metrics.Power.Value,
MaxPowerLimit: device.Metrics.PowerLimit.Value,
MemUsed: device.Metrics.MemoryUsed.Value,
MemTotal: device.Metrics.MemoryTotal.Value,
FanSpeed: device.Metrics.FanSpeed.Value,
}
if len(device.Processes) == 0 {
if device.ProcessStatus != "ok" {
return item
}
processes := make([]dto.GPUProcess, 0, len(device.Processes))
@@ -695,3 +983,14 @@ func loadHostSysPath() string {
}
return hostSys
}
func sortedMonitorOptions(optionSet map[string]struct{}) []string {
options := make([]string, 0, len(optionSet))
for name := range optionSet {
if len(name) != 0 && name != "all" {
options = append(options, name)
}
}
sort.Strings(options)
return append([]string{"all"}, options...)
}
+287
View File
@@ -0,0 +1,287 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/vllm"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/robfig/cron/v3"
"golang.org/x/sync/errgroup"
)
var vllmMonitorMutex sync.Mutex
var vllmMetricsClient = &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext, IdleConnTimeout: 30 * time.Second},
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
}
func (m *MonitorService) LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) {
data := dto.MonitorVLLMData{Points: []model.MonitorVLLM{}}
if !req.EndTime.After(req.StartTime) {
return data, fmt.Errorf("invalid vLLM history request")
}
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return data, err
}
if install.App.Key != "vllm" {
return data, fmt.Errorf("not a vLLM installation")
}
loc, err := time.LoadLocation(common.LoadTimeZoneByCmd())
if err != nil {
return data, err
}
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
data.SampleCount, err = vllmMonitorRepo.Count(req.AppInstallID, req.StartTime, req.EndTime)
if err != nil || data.SampleCount == 0 {
return data, err
}
if data.SampleCount > 1200 {
data.BucketSeconds = (req.EndTime.Unix() - req.StartTime.Unix() + 600) / 600
}
points, err := vllmMonitorRepo.History(req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation)
if err != nil {
return data, err
}
if data.BucketSeconds > 0 {
if req.Aggregation != "max" {
for i := range points {
if err := vllm.AggregateHistograms(points[i].HistogramSamples, &points[i].MonitorVLLM); err != nil {
return data, err
}
}
}
next := 0
for bucket := int64(0); bucket <= (req.EndTime.Unix()-req.StartTime.Unix())/data.BucketSeconds; bucket++ {
var point model.MonitorVLLM
if next < len(points) && points[next].Bucket == bucket {
point = points[next].MonitorVLLM
next++
}
point.AppInstallID = req.AppInstallID
point.CreatedAt = time.Unix(req.StartTime.Unix()+bucket*data.BucketSeconds, 0).In(loc)
if bucket == 0 {
point.CreatedAt = req.StartTime
}
data.Points = append(data.Points, point)
}
} else {
setting, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
if err != nil {
return data, err
}
intervalSeconds, err := strconv.Atoi(setting)
if err != nil || intervalSeconds <= 0 {
return data, fmt.Errorf("invalid vLLM monitoring interval: %s", setting)
}
interval := time.Duration(intervalSeconds) * time.Second
for i, point := range points {
if i > 0 && point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: points[i-1].CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
}
data.Points = append(data.Points, point.MonitorVLLM)
}
}
return data, nil
}
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error) {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return model.MonitorVLLM{}, err
}
if install.App.Key != "vllm" {
return model.MonitorVLLM{}, fmt.Errorf("not a vLLM installation")
}
previous, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
return previous, nil
}
timer := time.NewTimer(time.Second)
defer timer.Stop()
select {
case <-ctx.Done():
return model.MonitorVLLM{}, ctx.Err()
case <-timer.C:
}
point, err := collectVLLMMetrics(ctx, install, previous)
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
}
return point, nil
}
func (m *MonitorService) CleanVLLMMonitor(req dto.MonitorVLLMClean) error {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return err
}
if install.App.Key != "vllm" {
return fmt.Errorf("not a vLLM installation")
}
vllmMonitorMutex.Lock()
defer vllmMonitorMutex.Unlock()
return vllmMonitorRepo.CleanTarget(req.AppInstallID)
}
func StartVLLMMonitor(interval string) error {
seconds, err := strconv.Atoi(interval)
if err != nil || seconds < 10 || seconds > 43200 {
return fmt.Errorf("invalid vLLM monitoring interval: %s", interval)
}
service := &MonitorService{}
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveVLLMData))
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
if err != nil {
return err
}
if global.VLLMMonitorCronID != 0 {
global.Cron.Remove(global.VLLMMonitorCronID)
}
global.VLLMMonitorCronID = id
return nil
}
func (m *MonitorService) saveVLLMData() {
if !vllmMonitorMutex.TryLock() {
return
}
defer vllmMonitorMutex.Unlock()
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
if err != nil {
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
return
}
if status != constant.StatusEnable {
return
}
retention, err := settingRepo.GetValueByKey("VLLMMonitorStoreDays")
if err != nil {
global.LOG.Errorf("Load vLLM monitoring retention failed: %v", err)
return
}
days, err := strconv.Atoi(retention)
if err != nil || days < 1 {
global.LOG.Errorf("Invalid vLLM monitoring retention: %s", retention)
return
}
if err := vllmMonitorRepo.DeleteBefore(time.Now().AddDate(0, 0, -days)); err != nil {
global.LOG.Errorf("Clean vLLM monitoring data failed: %v", err)
}
apps, err := appRepo.GetBy(appRepo.WithKey("vllm"))
if err != nil {
global.LOG.Errorf("Load vLLM apps for monitoring failed: %v", err)
return
}
if len(apps) == 0 {
return
}
ids := make([]uint, 0, len(apps))
for _, app := range apps {
ids = append(ids, app.ID)
}
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppIdsIn(ids))
if err != nil {
global.LOG.Errorf("Load vLLM instances for monitoring failed: %v", err)
return
}
var group errgroup.Group
group.SetLimit(4)
for _, install := range installs {
group.Go(func() error {
previous, err := vllmMonitorRepo.Latest(install.ID)
if err != nil {
return err
}
point, err := collectVLLMMetrics(context.Background(), install, previous)
if err != nil {
global.LOG.Debugf("Collect vLLM metrics for %s failed: %v", install.Name, err)
}
return vllmMonitorRepo.Create(&point)
})
}
if err := group.Wait(); err != nil {
global.LOG.Errorf("Save vLLM monitoring data failed: %v", err)
}
}
func collectVLLMMetrics(ctx context.Context, install model.AppInstall, previous model.MonitorVLLM) (model.MonitorVLLM, error) {
point := model.MonitorVLLM{AppInstallID: install.ID, CreatedAt: time.Now(), Status: "unavailable"}
if install.HttpPort <= 0 || install.HttpPort > 65535 {
return point, fmt.Errorf("invalid vLLM port")
}
var env map[string]interface{}
if err := json.Unmarshal([]byte(install.Env), &env); err != nil {
return point, err
}
host, _ := env[constant.HostIP].(string)
host = strings.Trim(host, "[]")
switch host {
case "", "0.0.0.0":
host = "127.0.0.1"
case "::":
host = "::1"
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+net.JoinHostPort(host, strconv.Itoa(install.HttpPort))+"/metrics", nil)
if err != nil {
return point, err
}
response, err := vllmMetricsClient.Do(req)
if err != nil {
return point, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return point, fmt.Errorf("vLLM metrics returned HTTP %d", response.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024+1))
if err != nil {
return point, err
}
if len(body) > 8*1024*1024 {
return point, fmt.Errorf("vLLM metrics response too large")
}
metrics, err := vllm.Parse(bytes.NewReader(body))
if err != nil {
return point, err
}
point.CreatedAt = time.Now()
raw, err := json.Marshal(metrics)
if err != nil {
return point, err
}
var before vllm.Metrics
elapsed := point.CreatedAt.Sub(previous.CreatedAt).Seconds()
if previous.Status == "ok" && elapsed > 0 {
if err := json.Unmarshal([]byte(previous.RawMetrics), &before); err != nil {
return point, err
}
}
calculated, err := vllm.Calculate(metrics, before, elapsed)
if err != nil {
return point, err
}
calculated.AppInstallID = install.ID
calculated.CreatedAt = point.CreatedAt
calculated.RawMetrics = string(raw)
calculated.Status = "ok"
return calculated, nil
}
+72 -11
View File
@@ -7,6 +7,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net"
@@ -37,6 +38,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
)
const sshPath = "/etc/ssh/sshd_config"
@@ -438,15 +440,33 @@ func (u *SSHService) SyncRootCert() error {
global.LOG.Errorf("read pubic key of %s for sync failed, err: %v", item, err)
continue
}
cert.EncryptionMode = loadEncryptioMode(string(pubItem))
publicKey, _, _, rest, err := ssh.ParseAuthorizedKey(pubItem)
if err != nil || len(bytes.TrimSpace(rest)) != 0 {
continue
}
cert.EncryptionMode = loadSSHKeyType(publicKey)
if cert.EncryptionMode == "" {
continue
}
rootCerts = append(rootCerts, cert)
}
return hostRepo.SyncCert(rootCerts)
}
func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
if cmd.CheckIllegal(req.EncryptionMode, req.PassPhrase) {
return buserr.New("ErrCmdIllegal")
switch req.Mode {
case "generate":
switch req.EncryptionMode {
case "rsa", "ed25519", "ecdsa", "dsa":
default:
return buserr.WithName("ErrNotSupportType", req.EncryptionMode)
}
case "input", "import":
if err := validateSSHKeyPair(&req); err != nil {
return err
}
default:
return buserr.WithName("ErrNotSupportType", req.Mode)
}
certItem, _ := hostRepo.GetCert(repo.WithByName(req.Name))
if certItem.ID != 0 {
@@ -543,6 +563,9 @@ func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
}
func (u *SSHService) EditRootCert(req dto.RootCertOperate) error {
if err := validateSSHKeyPair(&req); err != nil {
return err
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("load current user failed, err: %v", err)
@@ -1681,22 +1704,60 @@ func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time {
return itemDate
}
func loadEncryptioMode(content string) string {
if strings.HasPrefix(content, "ssh-rsa") {
func loadSSHKeyType(publicKey ssh.PublicKey) string {
switch publicKey.Type() {
case ssh.KeyAlgoRSA:
return "rsa"
}
if strings.HasPrefix(content, "ssh-ed25519") {
case ssh.KeyAlgoED25519:
return "ed25519"
}
if strings.HasPrefix(content, "ssh-ecdsa") {
case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
return "ecdsa"
}
if strings.HasPrefix(content, "ssh-dsa") {
case ssh.KeyAlgoDSA:
return "dsa"
}
return ""
}
func validateSSHKeyPair(req *dto.RootCertOperate) error {
req.PublicKey = strings.TrimSpace(req.PublicKey)
req.PrivateKey = strings.TrimSpace(req.PrivateKey)
if req.PublicKey == "" || strings.ContainsAny(req.PublicKey, "\r\n") {
return buserr.New("ErrSSHPublicKey")
}
publicKey, comment, options, rest, err := ssh.ParseAuthorizedKey([]byte(req.PublicKey))
if err != nil || len(options) != 0 || len(bytes.TrimSpace(rest)) != 0 {
return buserr.New("ErrSSHPublicKey")
}
req.EncryptionMode = loadSSHKeyType(publicKey)
if req.EncryptionMode == "" {
return buserr.WithName("ErrNotSupportType", publicKey.Type())
}
block, rest := pem.Decode([]byte(req.PrivateKey))
if !strings.HasPrefix(req.PrivateKey, "-----BEGIN ") || block == nil || len(bytes.TrimSpace(rest)) != 0 {
return buserr.New("ErrSSHPrivateKey")
}
signer, err := ssh.ParsePrivateKey([]byte(req.PrivateKey))
var missingPassphrase *ssh.PassphraseMissingError
if errors.As(err, &missingPassphrase) {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(req.PrivateKey), []byte(req.PassPhrase))
} else if err == nil {
req.PassPhrase = ""
}
if err != nil {
return buserr.New("ErrSSHPrivateKey")
}
if !bytes.Equal(signer.PublicKey().Marshal(), publicKey.Marshal()) {
return buserr.New("ErrSSHKeyMismatch")
}
req.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey)))
if comment != "" {
req.PublicKey += " " + comment
}
req.PublicKey += "\n"
req.PrivateKey += "\n"
return nil
}
func updateLocalConn(newPort uint) error {
conn, _ := settingRepo.GetValueByKey("LocalSSHConn")
if len(conn) == 0 {
+1 -1
View File
@@ -154,7 +154,7 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e
logPath := path.Join(global.Dir.TaskDir, taskScope, taskID+".log")
logger := logrus.New()
logger.SetFormatter(&SimpleFormatter{})
logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, constant.FilePerm)
logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, constant.FilePerm)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %w", err)
}
+31 -4
View File
@@ -644,6 +644,13 @@
"formatZH": "创建容器网络 name",
"formatEN": "create container network [name]"
},
"/containers/network/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理未使用的容器网络",
"formatEN": "Clean unused container networks"
},
"/containers/network/del": {
"bodyKeys": [
"names"
@@ -2486,6 +2493,17 @@
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/core/xpack/vms/disks/resize": {
"bodyKeys": [
"name",
"target",
"sizeGiB"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
},
"/core/xpack/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
@@ -4028,12 +4046,21 @@
},
"/runtimes/operate": {
"bodyKeys": [
"id"
"ID"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作运行环境 [id]",
"formatEN": "Operate runtime [id]"
"beforeFunctions": [
{
"input_column": "id",
"input_value": "ID",
"isList": false,
"db": "runtimes",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "操作运行环境 [name]",
"formatEN": "Operate runtime [name]"
},
"/runtimes/php/config": {
"bodyKeys": [
+22
View File
@@ -38,6 +38,28 @@ func Run() {
}
}
var gpuStatus, gpuInterval model.Setting
if err := global.DB.Where("key = ?", "GPUMonitorStatus").First(&gpuStatus).Error; err != nil {
global.LOG.Errorf("Load GPU monitoring status failed: %v", err)
} else if gpuStatus.Value == constant.StatusEnable {
if err := global.DB.Where("key = ?", "GPUMonitorInterval").First(&gpuInterval).Error; err != nil {
global.LOG.Errorf("Load GPU monitoring interval failed: %v", err)
} else if err := service.StartGPUMonitor(gpuInterval.Value); err != nil {
global.LOG.Errorf("Start GPU monitoring failed: %v", err)
}
}
var vllmStatus, vllmInterval model.Setting
if err := global.DB.Where("key = ?", "VLLMMonitorStatus").First(&vllmStatus).Error; err != nil {
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
} else if vllmStatus.Value == constant.StatusEnable {
if err := global.DB.Where("key = ?", "VLLMMonitorInterval").First(&vllmInterval).Error; err != nil {
global.LOG.Errorf("Load vLLM monitoring interval failed: %v", err)
} else if err := service.StartVLLMMonitor(vllmInterval.Value); err != nil {
global.LOG.Errorf("Start vLLM monitoring failed: %v", err)
}
}
if _, err := global.Cron.AddJob("@daily", job.NewWebsiteJob()); err != nil {
global.LOG.Errorf("can not add website corn job: %s", err.Error())
}
+11 -8
View File
@@ -14,12 +14,13 @@ import (
)
var (
DB *gorm.DB
MonitorDB *gorm.DB
GPUMonitorDB *gorm.DB
TaskDB *gorm.DB
CoreDB *gorm.DB
AlertDB *gorm.DB
DB *gorm.DB
MonitorDB *gorm.DB
GPUMonitorDB *gorm.DB
VLLMMonitorDB *gorm.DB
TaskDB *gorm.DB
CoreDB *gorm.DB
AlertDB *gorm.DB
LOG *logrus.Logger
CONF ServerConfig
@@ -29,8 +30,10 @@ var (
Dir SystemDir
Cron *cron.Cron
MonitorCronID cron.EntryID
Cron *cron.Cron
MonitorCronID cron.EntryID
VLLMMonitorCronID cron.EntryID
GPUMonitorCronID cron.EntryID
IsMaster bool
+17 -20
View File
@@ -1,14 +1,12 @@
module github.com/1Panel-dev/1Panel/agent
go 1.26.1
replace github.com/moby/go-archive => github.com/moby/go-archive v0.1.0
go 1.26.6
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/compose-spec/compose-go/v2 v2.14.0
github.com/compose-spec/compose-go/v2 v2.15.0
github.com/creack/pty v1.1.24
github.com/docker/cli v29.7.2+incompatible
github.com/docker/docker v28.5.2+incompatible
@@ -33,6 +31,7 @@ require (
github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/miekg/dns v1.1.73
github.com/minio/minio-go/v7 v7.3.0
github.com/moby/go-archive v0.3.0
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/opencontainers/image-spec v1.1.1
github.com/oschwald/maxminddb-golang v1.13.1
@@ -52,13 +51,13 @@ require (
github.com/tklauser/go-sysconf v0.4.0
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
github.com/upyun/go-sdk v2.1.0+incompatible
go.mongodb.org/mongo-driver/v2 v2.8.2
golang.org/x/crypto v0.55.0
go.mongodb.org/mongo-driver/v2 v2.9.0
golang.org/x/crypto v0.56.0
golang.org/x/net v0.58.0
golang.org/x/sync v0.22.0
golang.org/x/sync v0.23.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.41.0
golang.org/x/time v0.15.0
golang.org/x/time v0.16.0
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
gopkg.in/ini.v1 v1.67.3
gopkg.in/yaml.v3 v3.0.1
@@ -121,7 +120,7 @@ require (
github.com/go-acme/alidns-20150109/v5 v5.5.0 // indirect
github.com/go-acme/esa-20240910/v3 v3.4.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@@ -132,7 +131,6 @@ require (
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.205 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -155,11 +153,10 @@ require (
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/sequential v0.7.0 // indirect
github.com/moby/sys/user v0.4.1 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -209,18 +206,18 @@ require (
go.mongodb.org/mongo-driver v1.17.9 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/arch v0.26.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
google.golang.org/grpc v1.83.1 // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/fileutil v1.4.0 // indirect
modernc.org/libc v1.72.0 // indirect
+41 -40
View File
@@ -205,8 +205,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/compose-spec/compose-go/v2 v2.14.0 h1:uaJeo5B3+OVlu+Rx2qLBcAdXPEUUzm5nQrRiGJafRAQ=
github.com/compose-spec/compose-go/v2 v2.14.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg=
github.com/compose-spec/compose-go/v2 v2.15.0 h1:tdQw+eMyT+P6ZIb09JfcIVvbMmIa+PjST7cWezVLf00=
github.com/compose-spec/compose-go/v2 v2.15.0/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -307,8 +307,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -438,6 +438,7 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
@@ -597,16 +598,16 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/go-archive v0.3.0 h1:nos4BtzzUIqB406BgQnWGMI4qib9BZ8XUHU+ucv/n1c=
github.com/moby/go-archive v0.3.0/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
@@ -877,8 +878,8 @@ go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lL
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.mongodb.org/mongo-driver/v2 v2.8.2 h1:b6o2m7zL8g2URuO8urBedAylxojybKXNZTxgkOcl+2w=
go.mongodb.org/mongo-driver/v2 v2.8.2/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.mongodb.org/mongo-driver/v2 v2.9.0 h1:e2mQdOmbkiYz+dj3faM7lVDwl7WdnRD+g5VicafMhL0=
go.mongodb.org/mongo-driver/v2 v2.9.0/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -889,23 +890,23 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
@@ -951,8 +952,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1076,8 +1077,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1193,8 +1194,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1315,10 +1316,10 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478 h1:aLsVTW0lZ8+IY5u/ERjZSCvAmhuR7slKzyha3YikDNA=
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478/go.mod h1:YJAzKjfHIUHb9T+bfu8L7mthAp7VVXQBUs1PLdBWS7M=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1336,8 +1337,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+15
View File
@@ -649,6 +649,7 @@ CommonAlert: "Panel {{ .node }}{{ .ip }}: {{ .msg }}. Log in to view details."
NodeExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} nodes are abnormal. Log in to view details."
LicenseExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} licenses are abnormal. Log in to view details."
SSHAndPanelLoginAlert: "Panel {{ .node }}{{ .ip }}: abnormal {{ .name }} login from {{ .loginIp }}. Log in to view details."
CronJobSuccessAlert: "Panel {{ .node }}{{ .ip }}: scheduled task {{ .name }} completed successfully. Log in to view details."
# disk
DeviceNotFound: "Device {{ .name }} not found"
@@ -730,3 +731,17 @@ FileTaskRename: 'Target filename: {{ .name }}'
ErrFirewallBackendCleanupRequired: "The current backend {{ .current }} still contains 1Panel rules. Clean it up before switching to {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "IPv4 forwarding is disabled. Set net.ipv4.ip_forward=1 before using Docker's firewall backend."
ErrFirewallRuleSavedApplyFailed: "The new rule configuration was saved, but applying it to the firewall failed. Retry by synchronizing: {{ .detail }}"
ErrSSHPublicKey: "Enter a valid SSH public key"
ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase"
ErrSSHKeyMismatch: "The SSH public and private keys do not match"
NetworkCleanupDeleted: "Deleted network [{{ .name }}] ({{ .id }})"
NetworkCleanupProtected: "Skipped network [{{ .name }}] ({{ .id }}): reserved network"
NetworkCleanupConnected: "Skipped network [{{ .name }}] ({{ .id }}): containers connected or network in use; not deleted"
NetworkCleanupUnsupported: "Skipped network [{{ .name }}] ({{ .id }}): special network"
NetworkCleanupGone: "Skipped network [{{ .name }}] ({{ .id }}): already removed"
NetworkCleanupInspectFailed: "Failed to inspect network [{{ .name }}] ({{ .id }}); not deleted"
NetworkCleanupRemoveFailed: "Failed to remove network [{{ .name }}] ({{ .id }})"
NetworkCleanupSummary: "Network cleanup finished: deleted {{ .deleted }}, skipped {{ .skipped }}, failed {{ .failed }}"
NetworkCleanupPartialFailure: "Some networks could not be cleaned; see the task log"
+5
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .msg }}. Inicie sesión en el pa
NodeExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} nodos son anómalos. Inicie sesión en el panel para ver los detalles.'
LicenseExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} licencias son anómalas. Inicie sesión en el panel para ver los detalles.'
SSHAndPanelLoginAlert: 'Su Panel {{ .node }}{{ .ip }}, el inicio de sesión {{ .name }} desde {{ .loginIp }} es anómalo. Inicie sesión en el panel para ver los detalles.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: la tarea programada {{ .name }} se completó correctamente. Inicie sesión para ver los detalles.'
# disco
DeviceNotFound: 'Dispositivo {{ .name }} no encontrado'
@@ -730,3 +731,7 @@ FileTaskRename: 'Nombre del archivo de destino: {{ .name }}'
ErrFirewallBackendCleanupRequired: "El backend actual {{ .current }} aún contiene reglas de 1Panel. Elimínelas antes de cambiar a {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "El reenvío IPv4 está desactivado. Configure net.ipv4.ip_forward=1 antes de usar el backend de cortafuegos de Docker."
ErrFirewallRuleSavedApplyFailed: "Se guardó la configuración de la nueva regla, pero no se pudo aplicar al cortafuegos. Vuelva a intentarlo mediante la sincronización: {{ .detail }}"
ErrSSHPublicKey: "Introduce una clave pública SSH válida"
ErrSSHPrivateKey: "Clave privada SSH no válida o contraseña incorrecta; las claves cifradas requieren la contraseña original"
ErrSSHKeyMismatch: "Las claves pública y privada SSH no coinciden"
+6
View File
@@ -649,6 +649,8 @@ CommonAlert: "پنل {{ .node }}{{ .ip }}: {{ .msg }}. برای مشاهده ج
NodeExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} گره غیرعادی هستند. برای مشاهده جزئیات وارد شوید."
LicenseExceptionAlert: "پنل {{ .node }}{{ .ip }}: {{ .num }} مجوز غیرعادی است. برای مشاهده جزئیات وارد شوید."
SSHAndPanelLoginAlert: "پنل {{ .node }}{{ .ip }}: ورود غیرعادی {{ .name }} از {{ .loginIp }}. برای مشاهده جزئیات وارد شوید."
CronJobSuccessAlert: "پنل {{ .node }}{{ .ip }}: وظیفه زمان‌بندی‌شده {{ .name }} با موفقیت تکمیل شد. برای مشاهده جزئیات وارد شوید."
# دیسک
DeviceNotFound: "دستگاه {{ .name }} یافت نشد"
@@ -730,3 +732,7 @@ FileTaskRename: 'نام فایل مقصد: {{ .name }}'
ErrFirewallBackendCleanupRequired: "بک‌اند فعلی {{ .current }} هنوز شامل قوانین 1Panel است. پیش از تغییر به {{ .target }} آن‌ها را پاک کنید."
ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش از استفاده از بک‌اند فایروال Docker، مقدار net.ipv4.ip_forward=1 را تنظیم کنید."
ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگام‌سازی دوباره تلاش کنید: {{ .detail }}"
ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید"
ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاری‌شده به عبارت عبور اصلی نیاز دارند"
ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندارند"
+5
View File
@@ -649,6 +649,7 @@ CommonAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .msg }}。詳細
NodeExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のノードに異常が発生しています。詳細はパネルにログインして'
LicenseExceptionAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .num }} 個のライセンスに異常が発生しています。詳細はパネルにログインして'
SSHAndPanelLoginAlert: 'あなたの {{ .node }}{{ .ip }} パネル、{{ .loginIp }} からの {{ .name }} ログインに異常があります。詳細はパネルにログインして'
CronJobSuccessAlert: 'パネル {{ .node }}{{ .ip }}: スケジュールタスク {{ .name }} が正常に完了しました。詳細はパネルにログインして確認してください。'
# ディスク
DeviceNotFound: 'デバイス {{ .name }} が見つかりません'
@@ -730,3 +731,7 @@ FileTaskRename: '保存先ファイル名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "現在のバックエンド {{ .current }} に 1Panel ルールが残っています。{{ .target }} に切り替える前に削除してください。"
ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファイアウォールバックエンドを使用する前に net.ipv4.ip_forward=1 を設定してください。"
ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}"
ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください"
ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です"
ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません"
+5
View File
@@ -649,6 +649,7 @@ CommonAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .msg }}。자세한 내
NodeExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 노드에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
LicenseExceptionAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .num }}개의 라이센스에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
SSHAndPanelLoginAlert: '귀하의 {{ .node }}{{ .ip }} 패널, {{ .loginIp }}에서의 {{ .name }} 로그인에 이상이 있습니다. 자세한 내용은 패널에 로그인하십시오.'
CronJobSuccessAlert: '패널 {{ .node }}{{ .ip }}: 예약 작업 {{ .name }}이(가) 성공적으로 완료되었습니다. 로그인하여 자세한 내용을 확인하십시오.'
# 디스크
DeviceNotFound: '장치 {{ .name }} 을(를) 찾을 수 없습니다'
@@ -730,3 +731,7 @@ FileTaskRename: '대상 파일 이름: {{ .name }}'
ErrFirewallBackendCleanupRequired: "현재 백엔드 {{ .current }}에 1Panel 규칙이 남아 있습니다. {{ .target }}로 전환하기 전에 정리하세요."
ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다. Docker 방화벽 백엔드를 사용하기 전에 net.ipv4.ip_forward=1을 설정하세요."
ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}"
ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요"
ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다"
ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다"
+5
View File
@@ -639,6 +639,7 @@ CommonAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: {{ .msg }}. ເ
NodeExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໂນດ {{ .num }} ແຫ່ງຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
LicenseExceptionAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໃບອະນຸຍາດ {{ .num }} ສະບັບຜິດປົກກະຕິ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
SSHAndPanelLoginAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ມີການເຂົ້າສູ່ລະບົບ {{ .name }} ທີ່ຜິດປົກກະຕິຈາກ {{ .loginIp }}. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
CronJobSuccessAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ວຽກທີ່ຕັ້ງເວລາ {{ .name }} ສຳເລັດແລ້ວ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
#disk
DeviceNotFound: "ບໍ່ພົບອຸປະກອນ {{ .name }}"
@@ -721,3 +722,7 @@ FileTaskRename: 'ຊື່ໄຟລ໌ປາຍທາງ: {{ .name }}'
ErrFirewallBackendCleanupRequired: "ແບັກເອນປັດຈຸບັນ {{ .current }} ຍັງມີກົດຂອງ 1Panel. ກະລຸນາລຶບອອກກ່ອນປ່ຽນໄປ {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກປິດ. ກະລຸນາຕັ້ງ net.ipv4.ip_forward=1 ກ່ອນໃຊ້ແບັກເອນໄຟວໍຂອງ Docker."
ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}"
ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ"
ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ"
ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນຕົວ SSH ບໍ່ກົງກັນ"
+5
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .msg }}. Sila log masuk ke pan
NodeExceptionAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .num }} nod tidak normal. Sila log masuk ke panel untuk butiran lanjut.'
LicenseExceptionAlert: 'Panel {{ .node }}{{ .ip }} Anda, {{ .num }} lesen tidak normal. Sila log masuk ke panel untuk butiran lanjut.'
SSHAndPanelLoginAlert: 'Panel {{ .node }}{{ .ip }} Anda, log masuk {{ .name }} dari {{ .loginIp }} tidak normal. Sila log masuk ke panel untuk butiran lanjut.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: tugas berjadual {{ .name }} berjaya diselesaikan. Log masuk untuk melihat butiran.'
# cakera
DeviceNotFound: 'Peranti {{ .name }} tidak ditemui'
@@ -730,3 +731,7 @@ FileTaskRename: 'Nama fail sasaran: {{ .name }}'
ErrFirewallBackendCleanupRequired: "Bahagian belakang semasa {{ .current }} masih mengandungi peraturan 1Panel. Buangkannya sebelum beralih kepada {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "Pemajuan IPv4 dilumpuhkan. Tetapkan net.ipv4.ip_forward=1 sebelum menggunakan bahagian belakang tembok api Docker."
ErrFirewallRuleSavedApplyFailed: "Konfigurasi peraturan baharu telah disimpan, tetapi gagal digunakan pada tembok api. Cuba lagi melalui penyegerakan: {{ .detail }}"
ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah"
ErrSSHPrivateKey: "Kunci peribadi SSH tidak sah atau frasa laluan salah; kunci yang disulitkan memerlukan frasa laluan asal"
ErrSSHKeyMismatch: "Kunci awam dan peribadi SSH tidak sepadan"
+5
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .msg }}. Faça login no painel
NodeExceptionAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .num }} nós estão anormais. Faça login no painel para obter detalhes.'
LicenseExceptionAlert: 'Seu Painel {{ .node }}{{ .ip }}, {{ .num }} licenças estão anormais. Faça login no painel para obter detalhes.'
SSHAndPanelLoginAlert: 'Seu Painel {{ .node }}{{ .ip }}, o login {{ .name }} a partir de {{ .loginIp }} é anormal. Faça login no painel para obter detalhes.'
CronJobSuccessAlert: 'Painel {{ .node }}{{ .ip }}: a tarefa agendada {{ .name }} foi concluída com sucesso. Faça login para ver os detalhes.'
# disco
DeviceNotFound: 'Dispositivo {{ .name }} não encontrado'
@@ -730,3 +731,7 @@ FileTaskRename: 'Nome do arquivo de destino: {{ .name }}'
ErrFirewallBackendCleanupRequired: "O backend atual {{ .current }} ainda contém regras do 1Panel. Remova-as antes de mudar para {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "O encaminhamento IPv4 está desativado. Defina net.ipv4.ip_forward=1 antes de usar o backend de firewall do Docker."
ErrFirewallRuleSavedApplyFailed: "A configuração da nova regra foi salva, mas não pôde ser aplicada ao firewall. Tente novamente por meio da sincronização: {{ .detail }}"
ErrSSHPublicKey: "Informe uma chave pública SSH válida"
ErrSSHPrivateKey: "Chave privada SSH inválida ou senha incorreta; chaves criptografadas exigem a senha original"
ErrSSHKeyMismatch: "As chaves pública e privada SSH não correspondem"
+5
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .msg }}. Войдит
NodeExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} узлов работают с ошибками. Войдите в панель для получения деталей.'
LicenseExceptionAlert: 'Ваш панель {{ .node }}{{ .ip }}, {{ .num }} лицензий имеют ошибки. Войдите в панель для получения деталей.'
SSHAndPanelLoginAlert: 'Ваш панель {{ .node }}{{ .ip }}, вход {{ .name }} с адреса {{ .loginIp }} является аномальным. Войдите в панель для получения деталей.'
CronJobSuccessAlert: 'Панель {{ .node }}{{ .ip }}: запланированная задача {{ .name }} успешно завершена. Войдите для просмотра подробностей.'
# диск
DeviceNotFound: 'Устройство {{ .name }} не найдено'
@@ -730,3 +731,7 @@ FileTaskRename: 'Имя целевого файла: {{ .name }}'
ErrFirewallBackendCleanupRequired: "В текущем бэкенде {{ .current }} остались правила 1Panel. Удалите их перед переключением на {{ .target }}."
ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. Перед использованием бэкенда межсетевого экрана Docker установите net.ipv4.ip_forward=1."
ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}"
ErrSSHPublicKey: "Введите действительный открытый ключ SSH"
ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза"
ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не соответствуют друг другу"
+5
View File
@@ -649,6 +649,7 @@ CommonAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .msg }}. Detaylar için panelin
NodeExceptionAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .num }} düğüm anormal durumda. Detaylar için paneline giriş yapın.'
LicenseExceptionAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .num }} lisans anormal durumda. Detaylar için paneline giriş yapın.'
SSHAndPanelLoginAlert: 'Paneliniz {{ .node }}{{ .ip }}, {{ .loginIp }} adresinden {{ .name }} girişi anormal. Detaylar için paneline giriş yapın.'
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: zamanlanmış görev {{ .name }} başarıyla tamamlandı. Ayrıntıları görmek için giriş yapın.'
# disk
DeviceNotFound: 'Cihaz {{ .name }} bulunamadı'
@@ -730,3 +731,7 @@ FileTaskRename: 'Hedef dosya adı: {{ .name }}'
ErrFirewallBackendCleanupRequired: "Mevcut {{ .current }} arka ucunda hâlâ 1Panel kuralları var. {{ .target }} arka ucuna geçmeden önce bunları temizleyin."
ErrDockerIPv4ForwardingDisabled: "IPv4 yönlendirmesi devre dışı. Docker güvenlik duvarı arka ucunu kullanmadan önce net.ipv4.ip_forward=1 ayarını yapın."
ErrFirewallRuleSavedApplyFailed: "Yeni kural yapılandırması kaydedildi, ancak güvenlik duvarına uygulanamadı. Eşitleme yaparak yeniden deneyin: {{ .detail }}"
ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin"
ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir"
ErrSSHKeyMismatch: "SSH açık ve özel anahtarları eşleşmiyor"
+15
View File
@@ -649,6 +649,7 @@ CommonAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .msg }},詳情請登入
NodeExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個節點出現異常,詳情請登入面板檢視。'
LicenseExceptionAlert: '您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 個授權出現異常,詳情請登入面板檢視。'
SSHAndPanelLoginAlert: '您的 {{ .node }}{{ .ip }} 面板,來自 {{ .loginIp }} 的 {{ .name }} 登入出現異常,詳情請登入面板檢視。'
CronJobSuccessAlert: '您的 {{ .node }}{{ .ip }} 面板,排程任務 {{ .name }} 執行成功,詳情請登入面板檢視。'
# 磁碟
DeviceNotFound: '裝置 {{ .name }} 未找到'
@@ -730,3 +731,17 @@ FileTaskRename: '目標檔名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。"
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
ErrSSHPublicKey: "請輸入有效的 SSH 公鑰"
ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼"
ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符"
NetworkCleanupDeleted: "網路 [{{ .name }}] ({{ .id }}) 已刪除"
NetworkCleanupProtected: "跳過網路 [{{ .name }}] ({{ .id }}):預設保留網路,未刪除"
NetworkCleanupConnected: "跳過網路 [{{ .name }}] ({{ .id }}):仍有容器連接或正在使用,未刪除"
NetworkCleanupUnsupported: "跳過網路 [{{ .name }}] ({{ .id }}):特殊網路,未刪除"
NetworkCleanupGone: "跳過網路 [{{ .name }}] ({{ .id }}):網路已不存在"
NetworkCleanupInspectFailed: "網路 [{{ .name }}] ({{ .id }}) 檢查失敗,未刪除"
NetworkCleanupRemoveFailed: "網路 [{{ .name }}] ({{ .id }}) 刪除失敗"
NetworkCleanupSummary: "網路清理完成:已刪除 {{ .deleted }},已跳過 {{ .skipped }},失敗 {{ .failed }}"
NetworkCleanupPartialFailure: "部分網路清理失敗,請查看任務日誌"
+15
View File
@@ -649,6 +649,7 @@ CommonAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .msg }},请登录面板
NodeExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个节点存在异常,请登录面板查看详情。"
LicenseExceptionAlert: "您的 {{ .node }}{{ .ip }} 面板,{{ .num }} 个许可证存在异常,请登录面板查看详情。"
SSHAndPanelLoginAlert: "您的 {{ .node }}{{ .ip }} 面板,面板 {{ .name }} 登录 {{ .loginIp }} 异常,请登录面板查看详情。"
CronJobSuccessAlert: "您的 {{ .node }}{{ .ip }} 面板,计划任务-{{ .name }}执行成功,请登录面板查看详情。"
# 磁盘
DeviceNotFound: "设备 {{ .name }} 未找到"
@@ -730,3 +731,17 @@ FileTaskRename: '目标文件名:{{ .name }}'
ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。"
ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。"
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
ErrSSHPublicKey: "请输入有效的 SSH 公钥"
ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码"
ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配"
NetworkCleanupDeleted: "网络 [{{ .name }}] ({{ .id }}) 已删除"
NetworkCleanupProtected: "跳过网络 [{{ .name }}] ({{ .id }}):默认保留网络,未删除"
NetworkCleanupConnected: "跳过网络 [{{ .name }}] ({{ .id }}):仍有容器连接或正在使用,未删除"
NetworkCleanupUnsupported: "跳过网络 [{{ .name }}] ({{ .id }}):特殊网络,未删除"
NetworkCleanupGone: "跳过网络 [{{ .name }}] ({{ .id }}):网络已不存在"
NetworkCleanupInspectFailed: "网络 [{{ .name }}] ({{ .id }}) 检查失败,未删除"
NetworkCleanupRemoveFailed: "网络 [{{ .name }}] ({{ .id }}) 删除失败"
NetworkCleanupSummary: "网络清理完成:已删除 {{ .deleted }},已跳过 {{ .skipped }},失败 {{ .failed }}"
NetworkCleanupPartialFailure: "部分网络清理失败,请查看任务日志"
+1
View File
@@ -13,6 +13,7 @@ func Init() {
global.TaskDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "task.db"), "task")
global.MonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "monitor.db"), "monitor")
global.GPUMonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "gpu_monitor.db"), "gpu_monitor")
global.VLLMMonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "vllm_monitor.db"), "vllm_monitor")
global.AlertDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "alert.db"), "alert")
if _, err := os.Stat(path.Join(global.Dir.DbDir, "core.db")); err == nil {
+33 -1
View File
@@ -4,6 +4,7 @@ import (
"os"
"os/exec"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -13,6 +14,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/alert_push"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"gorm.io/gorm"
)
func Init() {
@@ -174,6 +176,36 @@ func initAlertTask() {
}
func initMonitorDB() {
_ = global.MonitorDB.AutoMigrate(&model.MonitorBase{}, &model.MonitorNetwork{}, &model.MonitorGPU{}, &model.MonitorIO{})
_ = global.MonitorDB.AutoMigrate(&model.MonitorBase{}, &model.MonitorNetwork{}, &model.MonitorIO{})
_ = global.GPUMonitorDB.AutoMigrate(&model.MonitorGPU{})
_ = global.TaskDB.AutoMigrate(&model.Task{})
// building indexes on large monitor tables can take seconds, keep it off the startup path;
// WAL mode leaves readers unblocked and busy_timeout covers the collector's inserts meanwhile
go ensureMonitorIndexes()
}
func ensureMonitorIndexes() {
indexes := []struct {
db *gorm.DB
stmt string
}{
// created_at alone serves unfiltered range queries and retention cleanup
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_bases_created ON monitor_bases(created_at)"},
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_ios_created ON monitor_ios(created_at)"},
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_networks_created ON monitor_networks(created_at)"},
{global.GPUMonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_gpus_created ON monitor_gpus(created_at)"},
// (name, created_at) serves per-device range queries and distinct name lookups
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_ios_name_created ON monitor_ios(name, created_at)"},
{global.MonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_networks_name_created ON monitor_networks(name, created_at)"},
{global.GPUMonitorDB, "CREATE INDEX IF NOT EXISTS idx_monitor_gpus_product_created ON monitor_gpus(product_name, created_at)"},
}
start := time.Now()
for _, index := range indexes {
if err := index.db.Exec(index.stmt).Error; err != nil {
global.LOG.Warnf("create monitor index failed, stmt: %s, err: %v", index.stmt, err)
}
}
if elapsed := time.Since(start); elapsed > time.Second {
global.LOG.Infof("monitor indexes ready, took %s", elapsed)
}
}
+3
View File
@@ -111,6 +111,9 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.SimplifyFirewallRulePolicy,
migrations.AddDockerPortGuardReadOnly,
migrations.MigrateFirewallPortWhitelistSources,
migrations.AddAcceleratorMetrics,
migrations.AddVLLMMonitor,
migrations.AddMonitorSettings,
}
}
+32
View File
@@ -1908,3 +1908,35 @@ var AddDockerPortGuardReadOnly = &gormigrate.Migration{
return tx.Migrator().CreateIndex(&model.DockerPortGuardPolicy{}, "idx_docker_port_guard_endpoint")
},
}
var AddAcceleratorMetrics = &gormigrate.Migration{
ID: "20260928-accelerator-vendor-metrics",
Migrate: func(tx *gorm.DB) error {
return global.GPUMonitorDB.AutoMigrate(&model.MonitorGPU{})
},
}
var AddVLLMMonitor = &gormigrate.Migration{
ID: "20260928-vllm-monitor",
Migrate: func(tx *gorm.DB) error {
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
},
}
var AddMonitorSettings = &gormigrate.Migration{
ID: "20260929-monitor-settings",
Migrate: func(tx *gorm.DB) error {
for _, key := range []string{"MonitorStatus", "MonitorInterval", "MonitorStoreDays"} {
var setting model.Setting
if err := tx.Where("key = ?", key).First(&setting).Error; err != nil {
return err
}
for _, prefix := range []string{"GPU", "VLLM"} {
if err := tx.Where("key = ?", prefix+key).FirstOrCreate(&model.Setting{Key: prefix + key, Value: setting.Value}).Error; err != nil {
return err
}
}
}
return nil
},
}
+3
View File
@@ -27,6 +27,9 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.GET("/gpu/load", baseApi.LoadGpuInfo)
aiToolsRouter.POST("/gpu/search", baseApi.LoadGPUMonitor)
aiToolsRouter.GET("/gpu/options", baseApi.GetCPUOptions)
aiToolsRouter.POST("/vllm/monitor/search", baseApi.LoadVLLMMonitor)
aiToolsRouter.POST("/vllm/monitor/current", baseApi.LoadVLLMCurrent)
aiToolsRouter.POST("/vllm/monitor/clean", baseApi.CleanVLLMMonitor)
aiToolsRouter.POST("/mcp/search", baseApi.PageMcpServers)
aiToolsRouter.POST("/mcp/server/detail", baseApi.LoadMcpServerDetail)
+1
View File
@@ -77,6 +77,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.GET("/network", baseApi.ListNetwork)
baRouter.POST("/network/del", baseApi.DeleteNetwork)
baRouter.POST("/network/clean", baseApi.CleanNetworks)
baRouter.POST("/network/search", baseApi.SearchNetwork)
baRouter.POST("/network", baseApi.CreateNetwork)
baRouter.GET("/volume", baseApi.ListVolume)
+21 -14
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
@@ -22,14 +23,6 @@ type providerResult struct {
}
func New() (bool, Client) {
return newClient()
}
func NewAll() (bool, Client) {
return New()
}
func newClient() (bool, Client) {
client := Client{}
if available, gpuClient := gpu.New(); available {
client.providers = append(client.providers, gpuProvider{client: gpuClient})
@@ -52,6 +45,11 @@ func (c Client) LoadInfo() (*Info, error) {
}
func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
results := make([]providerResult, len(c.providers))
var wg sync.WaitGroup
for index, item := range c.providers {
@@ -66,8 +64,11 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
}()
}
wg.Wait()
if err := ctx.Err(); err != nil {
return nil, err
}
snapshot := &Snapshot{DriverVersions: make(map[string]string)}
snapshot := &Snapshot{}
var (
errs []error
active []*ProviderSnapshot
@@ -78,6 +79,11 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
errs = append(errs, result.err)
continue
}
if result.snapshot != nil {
for _, warning := range result.snapshot.Warnings {
errs = append(errs, errors.New(warning))
}
}
if result.snapshot == nil || len(result.snapshot.Devices) == 0 {
continue
}
@@ -90,11 +96,8 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
if item.CudaVersion != "" {
snapshot.Info.CudaVersion = item.CudaVersion
}
if item.DriverVersion != "" {
snapshot.DriverVersions[item.Type] = item.DriverVersion
if item.Type == "xpu" {
xpuVersion = item.DriverVersion
}
if item.Type == "xpu" {
xpuVersion = item.DriverVersion
}
}
snapshot.Warnings = append(snapshot.Warnings, errs...)
@@ -102,6 +105,10 @@ func (c Client) Collect(ctx context.Context) (*Snapshot, error) {
return nil, fmt.Errorf("calling accelerator monitoring tools failed: %w", errors.Join(errs...))
}
snapshot.Info.CollectedAt = time.Now()
for _, err := range errs {
snapshot.Info.Warnings = append(snapshot.Info.Warnings, err.Error())
}
snapshot.Info.XPUDriverVersion = xpuVersion
snapshot.Info.Type, snapshot.Info.DriverVersion = mergeProviderMetadata(active)
return snapshot, nil
+3 -2
View File
@@ -1,6 +1,7 @@
package accelerator
import (
"math"
"strconv"
"strings"
@@ -18,11 +19,11 @@ func metric(display, unit string) Metric {
return result
}
parsed, err := strconv.ParseFloat(matched[1], 64)
if err != nil {
if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || (parsed < 0 && unit != "°C") {
return result
}
normalized, ok := convertMetricUnit(parsed, matched[2], unit)
if !ok {
if !ok || math.IsNaN(normalized) || math.IsInf(normalized, 0) || (unit == "%" && normalized > 100) {
return result
}
result.Value = &normalized
+79 -63
View File
@@ -12,26 +12,38 @@ import (
func normalizeGPU(item *gpu.Device) Device {
metrics := Metrics{
Utilization: metric(item.GPUUtil, "%"),
Temperature: metric(item.Temperature, "°C"),
Power: metric(item.PowerDraw, "W"),
PowerLimit: metric(item.MaxPowerLimit, "W"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
FanSpeed: metric(item.FanSpeed, "%"),
MemoryActivity: metric(item.MemoryActivity, "%"),
EncoderUtil: metric(item.EncoderUtil, "%"),
DecoderUtil: metric(item.DecoderUtil, "%"),
JPEGUtil: metric(item.JPEGUtil, "%"),
OFAUtil: metric(item.OFAUtil, "%"),
MediaUtil: metric(item.MediaUtil, "%"),
HotspotTemperature: metric(item.HotspotTemperature, "°C"),
FanRPM: metric(item.FanRPM, "RPM"),
MediaFrequency: metric(item.MediaFrequency, "MHz"),
Utilization: metric(item.GPUUtil, "%"),
Temperature: metric(item.Temperature, "°C"),
MemoryTemperature: metric(item.MemoryTemperature, "°C"),
Power: metric(item.PowerDraw, "W"),
PowerLimit: metric(item.PowerLimit, "W"),
Frequency: metric(item.Frequency, "MHz"),
MemoryFrequency: metric(item.MemoryFrequency, "MHz"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
FanSpeed: metric(item.FanSpeed, "%"),
}
device := Device{
ID: stableID(item.Type, item.BusID, strconv.FormatUint(uint64(item.Index), 10)),
Kind: KindGPU,
Vendor: item.Type,
Index: int(item.Index),
Name: item.ProductName,
Label: fmt.Sprintf("%d - %s", item.Index, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
GPU: item,
ProcessStatus: item.ProcessStatus,
ID: stableID(item.Type, item.UUID, stableID("pci", item.BusID, strconv.FormatUint(uint64(item.Index), 10))),
Kind: KindGPU,
Vendor: item.Type,
Index: int(item.Index),
Name: item.ProductName,
Label: fmt.Sprintf("%d - %s", item.Index, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
GPU: item,
}
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{
PID: process.PID,
@@ -45,26 +57,36 @@ func normalizeGPU(item *gpu.Device) Device {
func normalizeNPU(item *npu.Device) Device {
metrics := Metrics{
Utilization: metric(item.AICore, "%"),
Temperature: metric(item.Temperature, "°C"),
Power: metric(item.PowerDraw, "W"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
AICPUUtil: metric(item.AICPUUtil, "%"),
CtrlCPUUtil: metric(item.CtrlCPUUtil, "%"),
DDRBandwidth: metric(item.DDRBandwidth, "%"),
HBMBandwidth: metric(item.HBMBandwidth, "%"),
DDRUsed: metric(item.MemoryUsed, "MiB"),
DDRTotal: metric(item.MemoryTotal, "MiB"),
HBMUsed: metric(item.HBMUsed, "MiB"),
HBMTotal: metric(item.HBMTotal, "MiB"),
HugepagesUsed: metric(item.HugepagesUsed, "pages"),
HugepagesTotal: metric(item.HugepagesTotal, "pages"),
Utilization: metric(item.AICore, "%"),
Temperature: metric(item.Temperature, "°C"),
Power: metric(item.PowerDraw, "W"),
MemoryUsed: memoryMetric(item.MemUsed),
MemoryTotal: memoryMetric(item.MemTotal),
}
device := Device{
ID: fmt.Sprintf("ascend:%d:%d", item.NPUIndex, item.ChipIndex),
Kind: KindNPU,
Vendor: "ascend",
Index: int(item.Index),
NPUIndex: int(item.NPUIndex),
ChipIndex: int(item.ChipIndex),
Name: item.ProductName,
Label: fmt.Sprintf("NPU %d / Chip %d - %s", item.NPUIndex, item.ChipIndex, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
NPU: item,
ProcessStatus: item.ProcessStatus,
ID: fmt.Sprintf("%s:%d", stableID("ascend", item.BusID, strconv.FormatUint(uint64(item.NPUIndex), 10)), item.ChipIndex),
Kind: KindNPU,
Vendor: "ascend",
Index: int(item.Index),
NPUIndex: int(item.NPUIndex),
ChipIndex: int(item.ChipIndex),
Name: item.ProductName,
Label: fmt.Sprintf("NPU %d / Chip %d - %s", item.NPUIndex, item.ChipIndex, item.ProductName),
BusID: item.BusID,
Metrics: metrics,
NPU: item,
}
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{
PID: process.PID,
@@ -78,26 +100,32 @@ func normalizeNPU(item *npu.Device) Device {
func normalizeXPU(item *xpu.Device) Device {
metrics := Metrics{
Utilization: metric(item.Stats.GPUUtil, "%"),
Temperature: metric(item.Stats.Temperature, "°C"),
Power: metric(item.Stats.Power, "W"),
MemoryUsed: memoryMetric(item.Stats.MemoryUsed),
MemoryTotal: memoryMetric(item.Basic.Memory),
MemoryUtil: metric(item.Stats.MemoryUtil, "%"),
Frequency: metric(item.Stats.Frequency, "MHz"),
MediaUtil: metric(item.Stats.MediaUtil, "%"),
ComputeUtil: metric(item.Stats.ComputeUtil, "%"),
CopyUtil: metric(item.Stats.CopyUtil, "%"),
MediaFrequency: metric(item.Stats.MediaFrequency, "MHz"),
MemoryTemperature: metric(item.Stats.MemoryTemperature, "°C"),
MemoryBandwidth: metric(item.Stats.MemoryBandwidthUtil, "%"),
Utilization: metric(item.Stats.GPUUtil, "%"),
Temperature: metric(item.Stats.Temperature, "°C"),
Power: metric(item.Stats.Power, "W"),
MemoryUsed: memoryMetric(item.Stats.MemoryUsed),
MemoryTotal: memoryMetric(item.Basic.Memory),
MemoryUtil: metric(item.Stats.MemoryUtil, "%"),
Frequency: metric(item.Stats.Frequency, "MHz"),
}
device := Device{
ID: stableID("xpu", item.Basic.PciBdfAddress, strconv.Itoa(item.Basic.DeviceID)),
Kind: KindXPU,
Vendor: item.Basic.VendorName,
Index: item.Basic.DeviceID,
Name: item.Basic.DeviceName,
Label: fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName),
BusID: item.Basic.PciBdfAddress,
Metrics: metrics,
XPU: item,
ProcessStatus: item.ProcessStatus,
ID: stableID("xpu", item.Basic.UUID, stableID("pci", item.Basic.PciBdfAddress, strconv.Itoa(item.Basic.DeviceID))),
Kind: KindXPU,
Vendor: item.Basic.VendorName,
Index: item.Basic.DeviceID,
Name: item.Basic.DeviceName,
Label: fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName),
BusID: item.Basic.PciBdfAddress,
Metrics: metrics,
XPU: item,
}
device.Capabilities = capabilities(metrics)
for _, process := range item.Processes {
device.Processes = append(device.Processes, Process{
PID: strconv.Itoa(process.PID),
@@ -110,18 +138,6 @@ func normalizeXPU(item *xpu.Device) Device {
return device
}
func capabilities(metrics Metrics) Capabilities {
return Capabilities{
Utilization: metrics.Utilization.Available(),
Temperature: metrics.Temperature.Available(),
Power: metrics.Power.Available(),
PowerLimit: metrics.PowerLimit.Available(),
Memory: metrics.MemoryUsed.Available() || metrics.MemoryTotal.Available(),
FanSpeed: metrics.FanSpeed.Available(),
Frequency: metrics.Frequency.Available(),
}
}
func stableID(vendor, busID, fallback string) string {
if busID != "" && !strings.EqualFold(busID, "N/A") {
return vendor + ":" + busID
+17 -1
View File
@@ -2,6 +2,7 @@ package accelerator
import (
"context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
@@ -26,6 +27,7 @@ func (p gpuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
}
result := &ProviderSnapshot{
Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion,
CudaVersion: info.CudaVersion,
GPUs: info.Devices,
@@ -49,6 +51,7 @@ func (p npuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
}
result := &ProviderSnapshot{
Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion,
NPUs: info.Devices,
}
@@ -71,11 +74,24 @@ func (p xpuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
}
result := &ProviderSnapshot{
Type: info.Type,
Warnings: info.Warnings,
DriverVersion: info.DriverVersion,
XPUs: info.Devices,
}
for index := range result.XPUs {
result.Devices = append(result.Devices, normalizeXPU(&result.XPUs[index]))
parent := normalizeXPU(&result.XPUs[index])
result.Devices = append(result.Devices, parent)
for _, tile := range result.XPUs[index].Tiles {
raw := xpu.Device{Basic: result.XPUs[index].Basic, Stats: tile.Stats, ProcessStatus: "unavailable"}
raw.Basic.Memory = ""
raw.Basic.FreeMemory = ""
device := normalizeXPU(&raw)
device.ParentID = parent.ID
device.ID = fmt.Sprintf("%s:tile:%d", parent.ID, tile.TileID)
device.Label = fmt.Sprintf("%s / Tile %d", parent.Label, tile.TileID)
result.Devices = append(result.Devices, device)
}
}
return result, nil
}
+54 -34
View File
@@ -2,6 +2,7 @@ package accelerator
import (
"errors"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
@@ -17,6 +18,9 @@ const (
)
type Info struct {
CollectedAt time.Time `json:"collectedAt"`
Warnings []string `json:"warnings"`
Type string `json:"type"`
CudaVersion string `json:"cudaVersion"`
DriverVersion string `json:"driverVersion"`
@@ -44,25 +48,40 @@ func (m Metric) ValueOrZero() float64 {
}
type Metrics struct {
Utilization Metric
Temperature Metric
Power Metric
PowerLimit Metric
MemoryUsed Metric
MemoryTotal Metric
MemoryUtil Metric
FanSpeed Metric
Frequency Metric
}
MemoryActivity Metric
EncoderUtil Metric
DecoderUtil Metric
JPEGUtil Metric
OFAUtil Metric
MediaUtil Metric
ComputeUtil Metric
CopyUtil Metric
HotspotTemperature Metric
FanRPM Metric
AICPUUtil Metric
CtrlCPUUtil Metric
DDRUsed Metric
DDRTotal Metric
HBMUsed Metric
HBMTotal Metric
DDRBandwidth Metric
HBMBandwidth Metric
MemoryBandwidth Metric
MediaFrequency Metric
HugepagesUsed Metric
HugepagesTotal Metric
type Capabilities struct {
Utilization bool
Temperature bool
Power bool
PowerLimit bool
Memory bool
FanSpeed bool
Frequency bool
MemoryTemperature Metric
Utilization Metric
Temperature Metric
Power Metric
PowerLimit Metric
MemoryUsed Metric
MemoryTotal Metric
MemoryUtil Metric
FanSpeed Metric
Frequency Metric
MemoryFrequency Metric
}
type Process struct {
@@ -74,18 +93,19 @@ type Process struct {
}
type Device struct {
ID string
Kind Kind
Vendor string
Index int
NPUIndex int
ChipIndex int
Name string
Label string
BusID string
Metrics Metrics
Capabilities Capabilities
Processes []Process
ParentID string
ProcessStatus string
ID string
Kind Kind
Vendor string
Index int
NPUIndex int
ChipIndex int
Name string
Label string
BusID string
Metrics Metrics
Processes []Process
GPU *gpu.Device `json:"-"`
NPU *npu.Device `json:"-"`
@@ -93,10 +113,9 @@ type Device struct {
}
type Snapshot struct {
Info Info
Devices []Device
DriverVersions map[string]string
Warnings []error
Info Info
Devices []Device
Warnings []error
}
func (s Snapshot) Warning() error {
@@ -104,6 +123,7 @@ func (s Snapshot) Warning() error {
}
type ProviderSnapshot struct {
Warnings []string
Type string
DriverVersion string
CudaVersion string
+58 -17
View File
@@ -3,12 +3,13 @@ package gpu
import (
"context"
"fmt"
"math"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
@@ -32,16 +33,22 @@ func findAMDSMI() (string, bool) {
func (a amdSMI) LoadInfo(ctx context.Context) (*Info, error) {
var (
staticData string
metricData string
processData string
staticErr error
metricErr error
processErr error
wg sync.WaitGroup
staticData string
metricData string
extendedData string
extendedErr error
processData string
staticErr error
metricErr error
processErr error
wg sync.WaitGroup
)
wg.Add(3)
wg.Add(4)
go func() {
defer wg.Done()
extendedData, extendedErr = runAMDSMI(ctx, a.command, "metric", "--clock", "--ecc", "--json")
}()
go func() {
defer wg.Done()
staticData, staticErr = runAMDSMI(ctx, a.command, "static", "--asic", "--bus", "--driver", "--limit", "--json")
@@ -64,16 +71,25 @@ func (a amdSMI) LoadInfo(ctx context.Context) (*Info, error) {
return nil, fmt.Errorf("parsing %s static output failed: %w", a.command, err)
}
if metricErr == nil {
metricErr = applyAMDMetrics(info, metricData)
}
if metricErr != nil {
global.LOG.Warnf("calling %s metric failed, metrics will be omitted: %v", a.command, metricErr)
} else if err := applyAMDMetrics(info, metricData); err != nil {
global.LOG.Warnf("parsing %s metric output failed, metrics will be omitted: %v", a.command, err)
info.Warnings = append(info.Warnings, fmt.Sprintf("%s metrics: %v", a.command, metricErr))
}
if extendedErr == nil {
extendedErr = applyAMDMetrics(info, extendedData)
}
if extendedErr != nil {
info.Warnings = append(info.Warnings, fmt.Sprintf("%s extended metrics: %v", a.command, extendedErr))
}
if processErr == nil {
processErr = applyAMDProcesses(info, processData)
}
if processErr != nil {
global.LOG.Warnf("calling %s process failed, process information will be omitted: %v", a.command, processErr)
} else if err := applyAMDProcesses(info, processData); err != nil {
global.LOG.Warnf("parsing %s process output failed, process information will be omitted: %v", a.command, err)
info.Warnings = append(info.Warnings, fmt.Sprintf("%s processes: %v", a.command, processErr))
}
return info, nil
}
@@ -96,6 +112,9 @@ func parseAMDStatic(data string) (*Info, error) {
}
device := Device{
Type: "amd",
UUID: amdStringAt(row, "uuid", "asic.uuid"),
DriverVersion: amdStringAt(row, "driver.version", "driver_version", "amdgpu_version"),
ProcessStatus: "unavailable",
Index: index,
ProductName: amdStringAt(row, "asic.market_name", "market_name", "gpu_name"),
PersistenceMode: "N/A",
@@ -111,6 +130,7 @@ func parseAMDStatic(data string) (*Info, error) {
"limit.max_power_limit",
"limit.max_power",
),
PowerLimit: amdMetricAt(row, "W", "limit.ppt0.socket_power_limit", "limit.socket_power_limit"),
MemUsed: "N/A",
MemTotal: "N/A",
GPUUtil: "N/A",
@@ -150,11 +170,31 @@ func applyAMDMetrics(info *Info, data string) error {
continue
}
setAMDMetric(&device.GPUUtil, row, "%", "usage.gfx_activity", "usage.gfx", "gfx_activity", "gfx_usage")
setAMDMetric(&device.Temperature, row, "°C", "temperature.hotspot", "temperature.edge", "hotspot_temperature", "gpu_temperature", "gpu_temp")
setAMDMetric(&device.Temperature, row, "°C", "temperature.edge", "gpu_temperature", "gpu_temp")
setAMDMetric(&device.PowerDraw, row, "W", "power.socket_power", "socket_power", "power_usage")
setAMDMetric(&device.MemUsed, row, "MB", "mem_usage.used_vram", "vram.used", "used_vram", "vram_used")
setAMDMetric(&device.MemTotal, row, "MB", "mem_usage.total_vram", "vram.total", "total_vram", "vram_total")
setAMDMetric(&device.FanSpeed, row, "%", "fan.speed", "fan_speed")
setAMDMetric(&device.FanSpeed, row, "%", "fan.usage")
if amdMetricAt(row, "%", "fan.usage") == "" {
speed, speedErr := strconv.ParseFloat(amdStringAt(row, "fan.speed"), 64)
maximum, maxErr := strconv.ParseFloat(amdStringAt(row, "fan.max"), 64)
if speedErr == nil && maxErr == nil && !math.IsNaN(speed) && !math.IsInf(speed, 0) && maximum > 0 && !math.IsInf(maximum, 0) && speed >= 0 && speed <= maximum {
device.FanSpeed = fmt.Sprintf("%.2f %%", speed/maximum*100)
}
}
setAMDMetric(&device.FanRPM, row, "RPM", "fan.rpm")
setAMDMetric(&device.HotspotTemperature, row, "°C", "temperature.hotspot", "hotspot_temperature")
setAMDMetric(&device.MemoryTemperature, row, "°C", "temperature.mem")
setAMDMetric(&device.MemoryActivity, row, "%", "usage.umc_activity")
setAMDMetric(&device.MediaUtil, row, "%", "usage.mm_activity")
setAMDMetric(&device.Frequency, row, "MHz", "clock.gfx_0.clk")
setAMDMetric(&device.MemoryFrequency, row, "MHz", "clock.mem_0.clk")
correctable := amdStringAt(row, "ecc.total_correctable_count", "ecc.correctable_count")
uncorrectable := amdStringAt(row, "ecc.total_uncorrectable_count", "ecc.uncorrectable_count")
if correctable != "" || uncorrectable != "" {
device.ECCErrors = []ECCError{{Scope: "Total", Correctable: correctable, Uncorrectable: uncorrectable}}
}
if value := amdStringAt(row, "perf_level", "performance_level"); value != "" {
device.PerformanceState = value
}
@@ -177,6 +217,7 @@ func applyAMDProcesses(info *Info, data string) error {
if !ok {
continue
}
device.ProcessStatus = "ok"
processList, _ := amdValueAt(row, "process_list")
items := amdObjectList(processList)
if len(items) == 0 {
+4
View File
@@ -64,6 +64,7 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
if result.info == nil {
continue
}
merged.Warnings = append(merged.Warnings, result.info.Warnings...)
if result.info.Type != "" {
types = append(types, result.info.Type)
}
@@ -86,6 +87,9 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
if len(merged.Devices) == 0 && len(errs) > 0 {
return nil, fmt.Errorf("calling GPU monitoring tools failed: %w", errors.Join(errs...))
}
for _, err := range errs {
merged.Warnings = append(merged.Warnings, err.Error())
}
return merged, nil
}
+76 -69
View File
@@ -1,16 +1,12 @@
package gpu
import (
"bytes"
"context"
"encoding/xml"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
@@ -24,41 +20,10 @@ func (n nvidiaSMI) LoadInfo(ctx context.Context) (*Info, error) {
if err != nil {
return nil, fmt.Errorf("calling %s failed: %w", nvidiaSMICommand, err)
}
data := []byte(itemData)
version := "v11"
buf := bytes.NewBuffer(data)
decoder := xml.NewDecoder(buf)
for {
token, err := decoder.Token()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("reading token failed: %w", err)
}
d, ok := token.(xml.Directive)
if !ok {
continue
}
directive := string(d)
if !strings.HasPrefix(directive, "DOCTYPE") {
continue
}
parts := strings.Split(directive, " ")
s := strings.Trim(parts[len(parts)-1], "\" ")
if strings.HasPrefix(s, "nvsmi_device_") && strings.HasSuffix(s, ".dtd") {
version = strings.TrimSuffix(strings.TrimPrefix(s, "nvsmi_device_"), ".dtd")
} else {
global.LOG.Debugf("Cannot find schema version in %q", directive)
}
break
}
return parseNvidiaSMI(data, version)
return parseNvidiaSMI([]byte(itemData))
}
func parseNvidiaSMI(buf []byte, version string) (*Info, error) {
func parseNvidiaSMI(buf []byte) (*Info, error) {
var (
s nvidiaSMIResponse
info Info
@@ -72,42 +37,84 @@ func parseNvidiaSMI(buf []byte, version string) (*Info, error) {
info.DriverVersion = s.DriverVersion
for i := range s.Gpu {
gpuItem := Device{
Type: "nvidia",
Index: uint(i),
ProductName: s.Gpu[i].ProductName,
PersistenceMode: s.Gpu[i].PersistenceMode,
BusID: s.Gpu[i].ID,
DisplayActive: s.Gpu[i].DisplayActive,
ECC: s.Gpu[i].EccErrors.Volatile.DramUncorrectable,
FanSpeed: s.Gpu[i].FanSpeed,
Temperature: s.Gpu[i].Temperature.GpuTemp,
PerformanceState: s.Gpu[i].PerformanceState,
MemUsed: s.Gpu[i].FbMemoryUsage.Used,
MemTotal: s.Gpu[i].FbMemoryUsage.Total,
GPUUtil: s.Gpu[i].Utilization.GpuUtil,
ComputeMode: s.Gpu[i].ComputeMode,
MigMode: s.Gpu[i].MigMode.CurrentMig,
}
if version == "v12" || version == "v13" {
gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.PowerDraw
if gpuItem.PowerDraw == "" {
gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.InstantPowerDraw
}
gpuItem.MaxPowerLimit = s.Gpu[i].GpuPowerReadings.CurrentPowerLimit
} else {
gpuItem.PowerDraw = s.Gpu[i].PowerReadings.PowerDraw
gpuItem.MaxPowerLimit = s.Gpu[i].PowerReadings.MaxPowerLimit
Type: "nvidia",
MemoryActivity: s.Gpu[i].Utilization.MemoryUtil,
EncoderUtil: s.Gpu[i].Utilization.EncoderUtil,
DecoderUtil: s.Gpu[i].Utilization.DecoderUtil,
JPEGUtil: s.Gpu[i].Utilization.JpegUtil,
OFAUtil: s.Gpu[i].Utilization.OfaUtil,
MediaFrequency: s.Gpu[i].Clocks.VideoClock,
UUID: s.Gpu[i].UUID,
DriverVersion: s.DriverVersion,
Architecture: s.Gpu[i].ProductArchitecture,
Frequency: s.Gpu[i].Clocks.GraphicsClock,
MemoryFrequency: s.Gpu[i].Clocks.MemClock,
MemoryTemperature: s.Gpu[i].Temperature.MemoryTemp,
MemoryFree: s.Gpu[i].FbMemoryUsage.Free,
MemoryReserved: s.Gpu[i].FbMemoryUsage.Reserved,
PCIeGeneration: firstSMIValue(s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.DeviceCurrentLinkGen, s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.CurrentLinkGen),
PCIeMaxGeneration: firstSMIValue(s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.MaxDeviceLinkGen, s.Gpu[i].Pci.PciGpuLinkInfo.PcieGen.MaxLinkGen),
PCIeWidth: s.Gpu[i].Pci.PciGpuLinkInfo.LinkWidths.CurrentLinkWidth,
PCIeMaxWidth: s.Gpu[i].Pci.PciGpuLinkInfo.LinkWidths.MaxLinkWidth,
ProcessStatus: "unavailable",
Index: uint(i),
ProductName: s.Gpu[i].ProductName,
PersistenceMode: s.Gpu[i].PersistenceMode,
BusID: s.Gpu[i].ID,
DisplayActive: s.Gpu[i].DisplayActive,
ECC: s.Gpu[i].EccMode.CurrentEcc,
FanSpeed: s.Gpu[i].FanSpeed,
Temperature: s.Gpu[i].Temperature.GpuTemp,
PerformanceState: s.Gpu[i].PerformanceState,
MemUsed: s.Gpu[i].FbMemoryUsage.Used,
MemTotal: s.Gpu[i].FbMemoryUsage.Total,
GPUUtil: s.Gpu[i].Utilization.GpuUtil,
ComputeMode: s.Gpu[i].ComputeMode,
MigMode: s.Gpu[i].MigMode.CurrentMig,
}
gpuItem.ECCPending = s.Gpu[i].EccMode.PendingEcc
gpuItem.ECCErrors = []ECCError{
{Scope: "Volatile Total", Correctable: s.Gpu[i].EccErrors.Volatile.SingleBit.Total, Uncorrectable: s.Gpu[i].EccErrors.Volatile.DoubleBit.Total},
{Scope: "Aggregate Total", Correctable: s.Gpu[i].EccErrors.Aggregate.SingleBit.Total, Uncorrectable: s.Gpu[i].EccErrors.Aggregate.DoubleBit.Total},
for _, process := range s.Gpu[i].Processes.ProcessInfo {
gpuItem.Processes = append(gpuItem.Processes, Process{
PID: process.Pid,
Type: process.Type,
ProcessName: process.ProcessName,
UsedMemory: process.UsedMemory,
})
{Scope: "Volatile DRAM", Correctable: s.Gpu[i].EccErrors.Volatile.DramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Volatile.DramUncorrectable},
{Scope: "Volatile SRAM", Correctable: s.Gpu[i].EccErrors.Volatile.SramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Volatile.SramUncorrectable},
{Scope: "Aggregate DRAM", Correctable: s.Gpu[i].EccErrors.Aggregate.DramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Aggregate.DramUncorrectable},
{Scope: "Aggregate SRAM", Correctable: s.Gpu[i].EccErrors.Aggregate.SramCorrectable, Uncorrectable: s.Gpu[i].EccErrors.Aggregate.SramUncorrectable},
}
gpuItem.PowerDraw = firstSMIValue(s.Gpu[i].GpuPowerReadings.PowerDraw, s.Gpu[i].GpuPowerReadings.InstantPowerDraw, s.Gpu[i].PowerReadings.PowerDraw)
gpuItem.PowerLimit = firstSMIValue(s.Gpu[i].GpuPowerReadings.CurrentPowerLimit, s.Gpu[i].PowerReadings.EnforcedPowerLimit, s.Gpu[i].PowerReadings.PowerLimit)
gpuItem.MaxPowerLimit = firstSMIValue(s.Gpu[i].GpuPowerReadings.MaxPowerLimit, s.Gpu[i].PowerReadings.MaxPowerLimit)
gpuItem.DefaultPowerLimit = firstSMIValue(s.Gpu[i].GpuPowerReadings.DefaultPowerLimit, s.Gpu[i].PowerReadings.DefaultPowerLimit)
for _, event := range append(s.Gpu[i].ClocksEventReasons.Reasons, s.Gpu[i].ClocksThrottleReasons.Reasons...) {
if strings.EqualFold(strings.TrimSpace(event.Value), "Active") {
gpuItem.ClockEvents = append(gpuItem.ClockEvents, strings.TrimPrefix(strings.TrimPrefix(event.XMLName.Local, "clocks_event_reason_"), "clocks_throttle_reason_"))
}
}
if s.Gpu[i].Processes != nil && strings.TrimSpace(s.Gpu[i].Processes.Text) == "" {
gpuItem.ProcessStatus = "ok"
}
if s.Gpu[i].Processes != nil {
for _, process := range s.Gpu[i].Processes.ProcessInfo {
gpuItem.Processes = append(gpuItem.Processes, Process{
PID: process.Pid,
Type: process.Type,
ProcessName: process.ProcessName,
UsedMemory: process.UsedMemory,
})
}
}
info.Devices = append(info.Devices, gpuItem)
}
return &info, nil
}
func firstSMIValue(values ...string) string {
for _, value := range values {
value = strings.TrimSpace(value)
if value != "" && !strings.EqualFold(value, "N/A") && !strings.EqualFold(value, "Not Supported") {
return value
}
}
return ""
}
+30 -13
View File
@@ -1,9 +1,12 @@
package gpu
import "encoding/xml"
type nvidiaSMIResponse struct {
AttachedGpus string `xml:"attached_gpus"`
CudaVersion string `xml:"cuda_version"`
DriverVersion string `xml:"driver_version"`
XMLName xml.Name `xml:"nvidia_smi_log"`
AttachedGpus string `xml:"attached_gpus"`
CudaVersion string `xml:"cuda_version"`
DriverVersion string `xml:"driver_version"`
Gpu []struct {
ID string `xml:"id,attr"`
AccountedProcesses struct{} `xml:"accounted_processes"`
@@ -37,16 +40,17 @@ type nvidiaSMIResponse struct {
VideoClock string `xml:"video_clock"`
} `xml:"clocks"`
ClocksEventReasons struct {
ClocksEventReasonApplicationsClocksSetting string `xml:"clocks_event_reason_applications_clocks_setting"`
ClocksEventReasonDisplayClocksSetting string `xml:"clocks_event_reason_display_clocks_setting"`
ClocksEventReasonGpuIdle string `xml:"clocks_event_reason_gpu_idle"`
ClocksEventReasonHwPowerBrakeSlowdown string `xml:"clocks_event_reason_hw_power_brake_slowdown"`
ClocksEventReasonHwSlowdown string `xml:"clocks_event_reason_hw_slowdown"`
ClocksEventReasonHwThermalSlowdown string `xml:"clocks_event_reason_hw_thermal_slowdown"`
ClocksEventReasonSwPowerCap string `xml:"clocks_event_reason_sw_power_cap"`
ClocksEventReasonSwThermalSlowdown string `xml:"clocks_event_reason_sw_thermal_slowdown"`
ClocksEventReasonSyncBoost string `xml:"clocks_event_reason_sync_boost"`
Reasons []struct {
XMLName xml.Name
Value string `xml:",chardata"`
} `xml:",any"`
} `xml:"clocks_event_reasons"`
ClocksThrottleReasons struct {
Reasons []struct {
XMLName xml.Name
Value string `xml:",chardata"`
} `xml:",any"`
} `xml:"clocks_throttle_reasons"`
ComputeMode string `xml:"compute_mode"`
DefaultApplicationsClocks struct {
GraphicsClock string `xml:"graphics_clock"`
@@ -63,12 +67,24 @@ type nvidiaSMIResponse struct {
} `xml:"driver_model"`
EccErrors struct {
Aggregate struct {
SingleBit struct {
Total string `xml:"total"`
} `xml:"single_bit"`
DoubleBit struct {
Total string `xml:"total"`
} `xml:"double_bit"`
DramCorrectable string `xml:"dram_correctable"`
DramUncorrectable string `xml:"dram_uncorrectable"`
SramCorrectable string `xml:"sram_correctable"`
SramUncorrectable string `xml:"sram_uncorrectable"`
} `xml:"aggregate"`
Volatile struct {
SingleBit struct {
Total string `xml:"total"`
} `xml:"single_bit"`
DoubleBit struct {
Total string `xml:"total"`
} `xml:"double_bit"`
DramCorrectable string `xml:"dram_correctable"`
DramUncorrectable string `xml:"dram_uncorrectable"`
SramCorrectable string `xml:"sram_correctable"`
@@ -226,7 +242,8 @@ type nvidiaSMIResponse struct {
MinPowerLimit string `xml:"min_power_limit"`
MaxPowerLimit string `xml:"max_power_limit"`
} `xml:"power_readings"`
Processes struct {
Processes *struct {
Text string `xml:",chardata"`
ProcessInfo []struct {
Pid string `xml:"pid"`
Type string `xml:"type"`
+37
View File
@@ -1,6 +1,8 @@
package gpu
type Info struct {
Warnings []string `json:"warnings"`
CudaVersion string `json:"cudaVersion"`
DriverVersion string `json:"driverVersion"`
Type string `json:"type"`
@@ -8,7 +10,42 @@ type Info struct {
Devices []Device `json:"gpu"`
}
type ECCError struct {
Scope string `json:"scope"`
Correctable string `json:"correctable"`
Uncorrectable string `json:"uncorrectable"`
}
type Device struct {
ECCPending string `json:"eccPending"`
ECCErrors []ECCError `json:"eccErrors"`
MemoryActivity string `json:"memoryActivity"`
EncoderUtil string `json:"encoderUtil"`
DecoderUtil string `json:"decoderUtil"`
JPEGUtil string `json:"jpegUtil"`
OFAUtil string `json:"ofaUtil"`
MediaUtil string `json:"mediaUtil"`
HotspotTemperature string `json:"hotspotTemperature"`
FanRPM string `json:"fanRPM"`
MediaFrequency string `json:"mediaFrequency"`
UUID string `json:"uuid"`
DriverVersion string `json:"driverVersion"`
Architecture string `json:"architecture"`
Frequency string `json:"frequency"`
MemoryFrequency string `json:"memoryFrequency"`
MemoryTemperature string `json:"memoryTemperature"`
MemoryFree string `json:"memoryFree"`
MemoryReserved string `json:"memoryReserved"`
PowerLimit string `json:"powerLimit"`
DefaultPowerLimit string `json:"defaultPowerLimit"`
PCIeGeneration string `json:"pcieGeneration"`
PCIeMaxGeneration string `json:"pcieMaxGeneration"`
PCIeWidth string `json:"pcieWidth"`
PCIeMaxWidth string `json:"pcieMaxWidth"`
ProcessStatus string `json:"processStatus"`
ClockEvents []string `json:"clockEvents"`
Type string `json:"type"`
Index uint `json:"index"`
ProductName string `json:"productName"`
+61 -1
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -34,7 +35,31 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
if err != nil {
return nil, fmt.Errorf("calling %s failed: %w", ascendSMICommand, err)
}
return parseAscendSMI(itemData), nil
info := parseAscendSMI(itemData)
var wg sync.WaitGroup
warnings := make([]string, len(info.Devices))
for i := range info.Devices {
wg.Add(1)
go func(index int) {
defer wg.Done()
device := &info.Devices[index]
mgr := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(5*time.Second))
data, err := mgr.RunWithStdout(ascendSMICommand, "info", "-t", "usages", "-i", strconv.FormatUint(uint64(device.NPUIndex), 10), "-c", strconv.FormatUint(uint64(device.ChipIndex), 10))
if err != nil {
warnings[index] = fmt.Sprintf("npu-smi usages %d/%d: %v", device.NPUIndex, device.ChipIndex, err)
return
}
applyAscendUsages(device, data)
}(i)
}
wg.Wait()
for _, warning := range warnings {
if warning != "" {
info.Warnings = append(info.Warnings, warning)
}
}
return info, nil
}
func parseAscendSMI(data string) *Info {
@@ -158,6 +183,12 @@ func parseAscendSMI(data string) *Info {
pending = nil
}
for i := range info.Devices {
info.Devices[i].ProcessStatus = "unavailable"
if processSection {
info.Devices[i].ProcessStatus = "ok"
}
}
return info
}
@@ -208,6 +239,9 @@ func ascendMemoryPools(value, header string) (string, string, string, string) {
}
hbm := usage[len(usage)-1]
if !strings.Contains(normalizedHeader, "MEMORYUSAGE") {
memoryUsed, memoryTotal = "", ""
}
return memoryUsed, memoryTotal, hbm[0] + " MB", hbm[1] + " MB"
}
@@ -233,3 +267,29 @@ func ascendValueWithUnit(value, unit string) string {
}
return value + " " + unit
}
func applyAscendUsages(device *Device, data string) {
for _, line := range strings.Split(data, "\n") {
key, value, ok := strings.Cut(line, ":")
if !ok {
continue
}
key = strings.ToLower(strings.Join(strings.Fields(key), ""))
value = strings.TrimSpace(value)
switch key {
case "aicoreusagerate(%)":
usage, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(value, "%")), 64)
if err == nil && usage >= 0 && usage <= 100 {
device.AICore = ascendValueWithUnit(value, "%")
}
case "aicpuusagerate(%)":
device.AICPUUtil = ascendValueWithUnit(value, "%")
case "ctrlcpuusagerate(%)":
device.CtrlCPUUtil = ascendValueWithUnit(value, "%")
case "ddrbandwidthusagerate(%)", "memorybandwidthusagerate(%)":
device.DDRBandwidth = ascendValueWithUnit(value, "%")
case "hbmbandwidthusagerate(%)":
device.HBMBandwidth = ascendValueWithUnit(value, "%")
}
}
}
+8
View File
@@ -1,12 +1,20 @@
package npu
type Info struct {
Warnings []string `json:"warnings"`
Type string `json:"type"`
DriverVersion string `json:"driverVersion"`
Devices []Device `json:"npu"`
}
type Device struct {
AICPUUtil string `json:"aiCPUUtil"`
CtrlCPUUtil string `json:"ctrlCPUUtil"`
DDRBandwidth string `json:"ddrBandwidth"`
HBMBandwidth string `json:"hbmBandwidth"`
ProcessStatus string `json:"processStatus"`
Type string `json:"type"`
Index uint `json:"index"`
NPUIndex uint `json:"npuIndex"`
+335
View File
@@ -0,0 +1,335 @@
package vllm
import (
"bufio"
"encoding/json"
"fmt"
"io"
"math"
"sort"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/utils/re"
)
type Metrics map[string]map[string]float64
type histogram struct {
Sum float64 `json:"sum"`
Count float64 `json:"count"`
Buckets map[string]float64 `json:"buckets,omitempty"`
}
func Parse(reader io.Reader) (Metrics, error) {
metrics := Metrics{}
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 4096), 1024*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "vllm:") {
continue
}
match := re.GetRegex(re.VLLMMetricSamplePattern).FindStringSubmatch(line)
if match == nil {
return nil, fmt.Errorf("invalid vLLM metric sample")
}
name := strings.TrimPrefix(match[1], "vllm:")
switch name {
case "num_requests_running", "num_requests_waiting", "kv_cache_usage_perc", "gpu_cache_usage_perc", "prompt_tokens_total", "generation_tokens_total", "request_success_total":
default:
base := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(name, "_sum"), "_count"), "_bucket")
if base == name {
continue
}
switch base {
case "time_to_first_token_seconds", "time_per_output_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds":
default:
continue
}
}
value, err := strconv.ParseFloat(match[3], 64)
if err != nil {
return nil, fmt.Errorf("invalid vLLM metric value: %w", err)
}
if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 {
continue
}
if metrics[name] == nil {
metrics[name] = map[string]float64{}
}
labels := re.GetRegex(re.VLLMMetricLabelPattern).FindAllStringSubmatch(match[2], -1)
parts := make([]string, 0, len(labels))
for _, label := range labels {
parts = append(parts, label[1]+"="+label[2])
}
sort.Strings(parts)
metrics[name]["{"+strings.Join(parts, ",")+"}"] = value
}
if err := scanner.Err(); err != nil {
return nil, err
}
if len(metrics) == 0 {
return nil, fmt.Errorf("no supported vLLM metrics")
}
return metrics, nil
}
func Calculate(current, previous Metrics, elapsed float64) (model.MonitorVLLM, error) {
result := model.MonitorVLLM{}
for name, target := range map[string]**float64{
"num_requests_running": &result.Running,
"num_requests_waiting": &result.Waiting,
} {
if len(current[name]) == 0 {
continue
}
value := 0.0
for _, sample := range current[name] {
value += sample
}
*target = &value
}
cache := current["kv_cache_usage_perc"]
if len(cache) == 0 {
cache = current["gpu_cache_usage_perc"]
}
if len(cache) > 0 {
value := 0.0
for _, sample := range cache {
value += sample
}
value = value / float64(len(cache)) * 100
result.CacheUsage = &value
}
if elapsed <= 0 {
return result, nil
}
for name, target := range map[string]**float64{
"prompt_tokens_total": &result.PromptThroughput,
"generation_tokens_total": &result.GenerationThroughput,
"request_success_total": &result.RequestThroughput,
} {
if value := counterDelta(current[name], previous[name]); value != nil {
rate := *value / elapsed
*target = &rate
}
}
histograms := map[string]histogram{}
for _, name := range []string{"time_to_first_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds"} {
source := name
if name == "inter_token_latency_seconds" && len(current[name+"_count"]) == 0 {
source = "time_per_output_token_seconds"
}
sum := counterDelta(current[source+"_sum"], previous[source+"_sum"])
count := counterDelta(current[source+"_count"], previous[source+"_count"])
if sum == nil || count == nil {
continue
}
histograms[name] = histogram{Sum: *sum, Count: *count, Buckets: bucketDeltas(current[source+"_bucket"], previous[source+"_bucket"], *count)}
}
applyHistograms(&result, histograms)
encoded, err := json.Marshal(histograms)
if err != nil {
return model.MonitorVLLM{}, err
}
result.HistogramDeltas = string(encoded)
return result, nil
}
func AggregateHistograms(encoded string, result *model.MonitorVLLM) error {
var samples []map[string]histogram
if err := json.Unmarshal([]byte(encoded), &samples); err != nil {
return err
}
merged := map[string]histogram{}
for _, sample := range samples {
for name, item := range sample {
if item.Count == 0 {
continue
}
total, exists := merged[name]
if !exists {
merged[name] = item
continue
}
total.Sum += item.Sum
total.Count += item.Count
if len(total.Buckets) != len(item.Buckets) {
total.Buckets = nil
}
for bound, count := range total.Buckets {
addition, ok := item.Buckets[bound]
if !ok {
total.Buckets = nil
break
}
total.Buckets[bound] = count + addition
}
merged[name] = total
}
}
applyHistograms(result, merged)
return nil
}
func applyHistograms(result *model.MonitorVLLM, histograms map[string]histogram) {
for _, item := range []struct {
name string
mean **float64
quantiles []**float64
}{
{"time_to_first_token_seconds", &result.TimeToFirstToken, []**float64{&result.TimeToFirstTokenP50, &result.TimeToFirstTokenP90, &result.TimeToFirstTokenP95, &result.TimeToFirstTokenP99}},
{"inter_token_latency_seconds", &result.TimePerOutputToken, []**float64{&result.TimePerOutputTokenP50, &result.TimePerOutputTokenP90, &result.TimePerOutputTokenP95, &result.TimePerOutputTokenP99}},
{"e2e_request_latency_seconds", &result.RequestLatency, []**float64{&result.RequestLatencyP50, &result.RequestLatencyP90, &result.RequestLatencyP95, &result.RequestLatencyP99}},
{"request_prefill_time_seconds", &result.PrefillTime, nil},
{"request_decode_time_seconds", &result.DecodeTime, nil},
} {
histogram, ok := histograms[item.name]
if !ok || histogram.Count <= 0 {
continue
}
mean := histogram.Sum / histogram.Count
*item.mean = &mean
if len(item.quantiles) == 0 {
continue
}
quantiles := histogramQuantiles(histogram.Buckets)
for i, target := range item.quantiles {
*target = quantiles[i]
}
}
}
func counterDelta(current, previous map[string]float64) *float64 {
if len(current) == 0 || len(current) != len(previous) {
return nil
}
value := 0.0
for label, sample := range current {
before, ok := previous[label]
if !ok || sample < before {
return nil
}
value += sample - before
}
if math.IsInf(value, 0) || math.IsNaN(value) {
return nil
}
return &value
}
func bucketDeltas(current, previous map[string]float64, count float64) map[string]float64 {
if len(current) == 0 || len(current) != len(previous) {
return nil
}
groups := map[string]map[string]float64{}
for labels, sample := range current {
before, ok := previous[labels]
if !ok || sample < before {
return nil
}
bound := ""
var identity []string
for _, label := range re.GetRegex(re.VLLMMetricLabelPattern).FindAllStringSubmatch(labels, -1) {
if label[1] == "le" {
raw, err := strconv.Unquote(label[2])
if err != nil {
return nil
}
upper, err := strconv.ParseFloat(raw, 64)
if err != nil || math.IsNaN(upper) || upper < 0 {
return nil
}
bound = strconv.FormatFloat(upper, 'g', -1, 64)
} else {
identity = append(identity, label[1]+"="+label[2])
}
}
if bound == "" {
return nil
}
group := strings.Join(identity, ",")
if groups[group] == nil {
groups[group] = map[string]float64{}
}
if _, exists := groups[group][bound]; exists {
return nil
}
groups[group][bound] = sample - before
}
result := map[string]float64{}
for _, buckets := range groups {
if _, ok := buckets["+Inf"]; !ok || len(buckets) < 2 {
return nil
}
if len(result) > 0 && len(result) != len(buckets) {
return nil
}
for bound := range result {
if _, ok := buckets[bound]; !ok {
return nil
}
}
bounds := make([]float64, 0, len(buckets))
for bound := range buckets {
value, _ := strconv.ParseFloat(bound, 64)
bounds = append(bounds, value)
}
sort.Float64s(bounds)
previousCount := 0.0
for _, bound := range bounds {
key := strconv.FormatFloat(bound, 'g', -1, 64)
value := buckets[key]
if value < previousCount {
if previousCount-value > 1e-12*(previousCount+value) {
return nil
}
value = previousCount
}
result[key] += value
previousCount = value
}
}
if math.Abs(result["+Inf"]-count) > 1e-12*(result["+Inf"]+count) {
return nil
}
return result
}
func histogramQuantiles(buckets map[string]float64) [4]*float64 {
var quantiles [4]*float64
if len(buckets) < 2 || buckets["+Inf"] <= 0 {
return quantiles
}
bounds := make([]float64, 0, len(buckets)-1)
for bound := range buckets {
value, err := strconv.ParseFloat(bound, 64)
if err != nil || math.IsNaN(value) || value < 0 {
return quantiles
}
if !math.IsInf(value, 1) {
bounds = append(bounds, value)
}
}
if len(bounds) == 0 {
return quantiles
}
sort.Float64s(bounds)
for i, q := range [...]float64{0.5, 0.9, 0.95, 0.99} {
rank := q * buckets["+Inf"]
lower, before := 0.0, 0.0
value := bounds[len(bounds)-1]
for _, upper := range bounds {
count := buckets[strconv.FormatFloat(upper, 'g', -1, 64)]
if count >= rank && count > before {
value = lower + (upper-lower)*(rank-before)/(count-before)
break
}
lower, before = upper, count
}
quantiles[i] = &value
}
return quantiles
}
+28 -2
View File
@@ -33,16 +33,29 @@ type discoveryInfo struct {
}
type DeviceLevelMetric struct {
MetricsType string `json:"metrics_type"`
Value float64 `json:"value"`
MetricsType string `json:"metrics_type"`
Value *float64 `json:"value"`
}
type TileMetrics struct {
TileID int `json:"tile_id"`
DataList []DeviceLevelMetric `json:"data_list"`
}
type TileStats struct {
TileID int `json:"tileID"`
Stats Stats `json:"stats"`
}
type DeviceStats struct {
TileLevel []TileMetrics `json:"tile_level"`
DeviceID int `json:"device_id"`
DeviceLevel []DeviceLevelMetric `json:"device_level"`
}
type Info struct {
Warnings []string `json:"warnings"`
Type string `json:"type"`
DriverVersion string `json:"driverVersion"`
@@ -50,12 +63,17 @@ type Info struct {
}
type Device struct {
Tiles []TileStats `json:"tiles"`
ProcessStatus string `json:"processStatus"`
Basic Basic `json:"basic"`
Stats Stats `json:"stats"`
Processes []Process `json:"processes"`
}
type Basic struct {
UUID string `json:"uuid"`
DeviceID int `json:"deviceID"`
DeviceName string `json:"deviceName"`
VendorName string `json:"vendorName"`
@@ -66,6 +84,14 @@ type Basic struct {
}
type Stats struct {
MediaUtil string `json:"mediaUtil"`
ComputeUtil string `json:"computeUtil"`
CopyUtil string `json:"copyUtil"`
MediaFrequency string `json:"mediaFrequency"`
MemoryTemperature string `json:"memoryTemperature"`
MemoryBandwidthUtil string `json:"memoryBandwidthUtil"`
Power string `json:"power"`
GPUUtil string `json:"gpuUtil"`
Frequency string `json:"frequency"`
+53 -13
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"sort"
"strconv"
"sync"
@@ -51,12 +52,15 @@ func (c Client) LoadInfoContext(ctx context.Context) (*Info, error) {
processData, err := cmdMgr.RunWithStdout(xpuSMICommand, "ps", "-j")
if err != nil {
global.LOG.Warnf("calling xpu-smi ps failed, process information will be omitted: %v", err)
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi ps: %v", err))
} else {
var psList DeviceUtilByProcList
if err := json.Unmarshal([]byte(processData), &psList); err != nil {
global.LOG.Warnf("processData json unmarshal failed, process information will be omitted: %v", err)
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi ps output: %v", err))
} else {
for i := range res.Devices {
res.Devices[i].ProcessStatus = "ok"
}
for _, ps := range psList.DeviceUtilByProcList {
process := Process{
PID: ps.ProcessID,
@@ -87,7 +91,9 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
defer wg.Done()
xpu := Device{
ProcessStatus: "unavailable",
Basic: Basic{
UUID: device.UUID,
DeviceID: device.DeviceID,
DeviceName: device.DeviceName,
VendorName: device.VendorName,
@@ -115,13 +121,19 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
wgCmd.Wait()
if xpuErr != nil {
global.LOG.Errorf("calling xpu-smi discovery failed for device %d, %v", device.DeviceID, xpuErr)
mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi device %d: %v", device.DeviceID, xpuErr))
res.Devices = append(res.Devices, xpu)
mu.Unlock()
return
}
var info discoveryDevice
if err := json.Unmarshal([]byte(xpuData), &info); err != nil {
global.LOG.Errorf("xpuData json unmarshal failed for device %d, err: %v", device.DeviceID, err)
mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi device %d output: %v", device.DeviceID, err))
res.Devices = append(res.Devices, xpu)
mu.Unlock()
return
}
@@ -142,13 +154,22 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
}
if statsErr != nil {
global.LOG.Warnf("calling xpu-smi stats failed for device %d, metrics will be omitted: %v", device.DeviceID, statsErr)
mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi stats %d: %v", device.DeviceID, statsErr))
mu.Unlock()
} else {
var stats DeviceStats
if err := json.Unmarshal([]byte(statsData), &stats); err != nil {
global.LOG.Warnf("statsData json unmarshal failed for device %d, metrics will be omitted: %v", device.DeviceID, err)
mu.Lock()
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi stats %d output: %v", device.DeviceID, err))
mu.Unlock()
} else {
loadStats(&xpu.Stats, stats.DeviceLevel)
for _, tile := range stats.TileLevel {
item := TileStats{TileID: tile.TileID}
loadStats(&item.Stats, tile.DataList)
xpu.Tiles = append(xpu.Tiles, item)
}
}
}
@@ -162,19 +183,38 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
func loadStats(stats *Stats, metrics []DeviceLevelMetric) {
for _, stat := range metrics {
if stat.Value == nil || math.IsNaN(*stat.Value) || math.IsInf(*stat.Value, 0) {
continue
}
value := *stat.Value
if value < 0 && stat.MetricsType != "XPUM_STATS_GPU_CORE_TEMPERATURE" && stat.MetricsType != "XPUM_STATS_MEMORY_TEMPERATURE" {
continue
}
switch stat.MetricsType {
case "XPUM_STATS_MEMORY_TEMPERATURE":
stats.MemoryTemperature = fmt.Sprintf("%.1f°C", value)
case "XPUM_STATS_ENGINE_GROUP_COMPUTE_ALL_UTILIZATION":
stats.ComputeUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_ENGINE_GROUP_MEDIA_ALL_UTILIZATION":
stats.MediaUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_ENGINE_GROUP_COPY_ALL_UTILIZATION":
stats.CopyUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_MEDIA_ENGINE_FREQUENCY":
stats.MediaFrequency = fmt.Sprintf("%.1fMHz", value)
case "XPUM_STATS_POWER":
stats.Power = fmt.Sprintf("%.1fW", stat.Value)
stats.Power = fmt.Sprintf("%.1fW", value)
case "XPUM_STATS_GPU_UTILIZATION":
stats.GPUUtil = fmt.Sprintf("%.1f%%", stat.Value)
stats.GPUUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_GPU_FREQUENCY":
stats.Frequency = fmt.Sprintf("%.1fMHz", stat.Value)
stats.Frequency = fmt.Sprintf("%.1fMHz", value)
case "XPUM_STATS_GPU_CORE_TEMPERATURE":
stats.Temperature = fmt.Sprintf("%.1f°C", stat.Value)
stats.Temperature = fmt.Sprintf("%.1f°C", value)
case "XPUM_STATS_MEMORY_USED":
stats.MemoryUsed = fmt.Sprintf("%.1f MiB", stat.Value)
case "XPUM_STATS_MEMORY_UTILIZATION", "XPUM_STATS_MEMORY_BANDWIDTH", "XPUM_STATS_MEMORY_BANDWIDTH_UTILIZATION":
stats.MemoryUtil = fmt.Sprintf("%.1f%%", stat.Value)
stats.MemoryUsed = fmt.Sprintf("%.1f MiB", value)
case "XPUM_STATS_MEMORY_UTILIZATION":
stats.MemoryUtil = fmt.Sprintf("%.1f%%", value)
case "XPUM_STATS_MEMORY_BANDWIDTH", "XPUM_STATS_MEMORY_BANDWIDTH_UTILIZATION":
stats.MemoryBandwidthUtil = fmt.Sprintf("%.1f%%", value)
}
}
}
+48 -7
View File
@@ -24,10 +24,10 @@ import (
"github.com/jinzhu/copier"
)
var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp"}
var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp", "syncIpGroup", "cleanLog"}
func CreateTaskScanEmailAlertLog(alert dto.AlertDTO, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo, emailConfig model.AlertConfig) error {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param))
params := CreateTaskAlertParams(pushAlert)
alertDetail := ProcessAlertDetail(alert, pushAlert.TaskName, params, method)
alertRule := ProcessAlertRule(alert)
create.AlertRule = alertRule
@@ -76,14 +76,14 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [
Encryption: emailInfo.Encryption,
Recipient: emailInfo.Recipient,
}
content := GetSendContent(alert.Type, params, agentInfo)
content := GetAlertLogContent(create, alert, params, agentInfo)
if content == "" {
content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title})
}
msg := email.EmailMessage{
Subject: i18n.GetMsgByKey("PanelAlertTitle"),
Body: content,
IsHTML: true,
IsHTML: GetCronJobType(alert.Type) != "cronJob",
}
if err = email.SendMail(smtpConfig, msg, transport); err != nil {
@@ -110,7 +110,7 @@ func CreateBarkAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []
return SaveAlertLog(create, &alertLog)
}
content := GetSendContent(alert.Type, params, agentInfo)
content := GetAlertLogContent(create, alert, params, agentInfo)
if content == "" {
content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title})
}
@@ -268,7 +268,7 @@ func ProcessAlertDetail(alert dto.AlertDTO, project string, params []dto.Param,
alertDetail := dto.AlertDetail{
Type: GetCronJobType(alert.Type),
SubType: alert.Type,
Title: alert.Title,
Title: TaskAlertTitle(alert.Type, alert.Title, project, params),
Method: method,
Project: project,
Params: params,
@@ -281,6 +281,14 @@ func ProcessAlertDetail(alert dto.AlertDTO, project string, params []dto.Param,
return string(marshal)
}
func TaskAlertTitle(alertType, title, project string, params []dto.Param) string {
if GetCronJobType(alertType) != "cronJob" || CronJobAlertResultFromParams(params) != CronJobAlertSuccess {
return title
}
name := cronJobTaskName(project, params)
return i18n.GetMsgWithMap("TaskSuccess", map[string]interface{}{"name": name})
}
func ProcessAlertRule(alert dto.AlertDTO) string {
marshal, err := json.Marshal(alert)
if err != nil {
@@ -324,6 +332,10 @@ func GetCronJobTypeName(cronJobType string) string {
module = "系统快照"
case "ntp":
module = "同步服务器时间"
case "syncIpGroup":
module = "同步 IP 组"
case "cleanLog":
module = "清理日志"
default:
}
return module
@@ -444,6 +456,30 @@ func isWithinTimeRange(savedTimeString string) bool {
}
func GetSendContent(alertType string, params []dto.Param, agentInfo *dto.AgentInfo) string {
return GetAlertDetailContent(dto.AlertDetail{Type: alertType, Params: params}, agentInfo)
}
func GetAlertLogContent(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, agentInfo *dto.AgentInfo) string {
detail := dto.AlertDetail{Type: alert.Type, Params: params}
var stored dto.AlertDetail
if json.Unmarshal([]byte(create.AlertDetail), &stored) == nil {
detail = stored
if detail.Type == "" {
detail.Type = alert.Type
}
if detail.Params == nil {
detail.Params = params
}
}
return GetAlertDetailContent(detail, agentInfo)
}
func GetAlertDetailContent(detail dto.AlertDetail, agentInfo *dto.AgentInfo) string {
alertType := detail.SubType
if alertType == "" {
alertType = detail.Type
}
params := detail.Params
switch GetCronJobType(alertType) {
case "ssl":
return i18n.GetMsgWithMap("SSLAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "day": getValueByIndex(params, "2"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
@@ -464,7 +500,12 @@ func GetSendContent(alertType string, params []dto.Param, agentInfo *dto.AgentIn
case "disk":
return i18n.GetMsgWithMap("DiskUsedAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "used": getValueByIndex(params, "2"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
case "cronJob":
return i18n.GetMsgWithMap("CronJobFailedAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
messageKey := "CronJobFailedAlert"
if CronJobAlertResultFromParams(params) == CronJobAlertSuccess {
messageKey = "CronJobSuccessAlert"
}
name := cronJobTaskName(detail.Project, params)
return i18n.GetMsgWithMap(messageKey, map[string]interface{}{"name": name, "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
case "clams":
return i18n.GetMsgWithMap("ClamAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "node": getNodeName(agentInfo), "ip": getNodeIp(agentInfo)})
case "panelLogin":
+130
View File
@@ -0,0 +1,130 @@
package alert
import (
"encoding/json"
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
)
const (
CronJobAlertFailed = "failed"
CronJobAlertSuccess = "success"
CronJobAlertBoth = "both"
)
func cronJobAlertParams(advanced string) (map[string]json.RawMessage, error) {
params := make(map[string]json.RawMessage)
if strings.TrimSpace(advanced) != "" {
if err := json.Unmarshal([]byte(advanced), &params); err != nil {
return nil, fmt.Errorf("invalid cronjob alert advanced parameters: %w", err)
}
}
if params == nil {
params = make(map[string]json.RawMessage)
}
return params, nil
}
func CronJobAlertTriggerMode(advanced string) (string, error) {
params, err := cronJobAlertParams(advanced)
if err != nil {
return CronJobAlertFailed, err
}
mode := CronJobAlertFailed
if raw, ok := params["alertTriggerMode"]; ok {
if err := json.Unmarshal(raw, &mode); err != nil {
return CronJobAlertFailed, fmt.Errorf("invalid cronjob alert trigger mode: %w", err)
}
}
switch mode {
case CronJobAlertFailed, CronJobAlertSuccess, CronJobAlertBoth:
return mode, nil
default:
return CronJobAlertFailed, fmt.Errorf("invalid cronjob alert trigger mode %q", mode)
}
}
func MergeCronJobAlertParams(previous, incoming string) (string, error) {
params, err := cronJobAlertParams(previous)
if err != nil {
return "", err
}
updates, err := cronJobAlertParams(incoming)
if err != nil {
return "", err
}
for key, value := range updates {
params[key] = value
}
if _, ok := params["alertTriggerMode"]; !ok {
params["alertTriggerMode"] = json.RawMessage(`"failed"`)
}
data, err := json.Marshal(params)
if err != nil {
return "", err
}
if _, err := CronJobAlertTriggerMode(string(data)); err != nil {
return "", err
}
return string(data), nil
}
func MatchCronJobAlertResult(advanced, result string) bool {
mode, err := CronJobAlertTriggerMode(advanced)
if err != nil {
return false
}
if result == "" {
result = CronJobAlertFailed
}
if result != CronJobAlertFailed && result != CronJobAlertSuccess {
return false
}
return mode == CronJobAlertBoth || mode == result
}
func CreateTaskAlertParams(pushAlert dto.PushAlert) []dto.Param {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param))
if GetCronJobType(pushAlert.AlertType) != "cronJob" {
return params
}
params = append(params, CreateCronJobResultParam(pushAlert.Result))
return params
}
func cronJobTaskName(project string, params []dto.Param) string {
if project != "" {
return project
}
if name := getValueByIndex(params, "taskName"); name != "" {
return name
}
return getValueByIndex(params, "1")
}
func CreateCronJobResultParam(result string) dto.Param {
value := "失败"
if result == CronJobAlertSuccess {
value = "成功"
}
return dto.Param{Index: "2", Key: "result", Value: value}
}
func CronJobAlertResultFromParams(params []dto.Param) string {
for _, param := range params {
if param.Index == "result" {
if param.Value == CronJobAlertSuccess {
return CronJobAlertSuccess
}
return CronJobAlertFailed
}
}
for _, param := range params {
if param.Index == "2" && param.Key == "result" && param.Value == "成功" {
return CronJobAlertSuccess
}
}
return CronJobAlertFailed
}
+2 -2
View File
@@ -46,7 +46,7 @@ func CreateTaskScanCustomWebhookAlertLog(
transport *http.Transport,
agentInfo *dto.AgentInfo,
) error {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param))
params := CreateTaskAlertParams(pushAlert)
alertInfo := info
alertInfo.Type = alertType
create.Type = GetCronJobType(alertType)
@@ -99,7 +99,7 @@ func customWebhookTemplateData(rawDetail string, agentInfo *dto.AgentInfo, occur
if businessType == "" {
return webhook_sender.TemplateData{}, errors.New("resolve custom webhook alert detail failed")
}
content := GetSendContent(businessType, detail.Params, agentInfo)
content := GetAlertDetailContent(detail, agentInfo)
if content == "" {
content = i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": detail.Title})
}
+4 -1
View File
@@ -26,6 +26,9 @@ func PushAlert(pushAlert dto.PushAlert) error {
}
var alert dto.AlertDTO
_ = copier.Copy(&alert, &alertInfo)
if alertUtil.GetCronJobType(pushAlert.AlertType) == "cronJob" && !alertUtil.MatchCronJobAlertResult(alert.AdvancedParams, pushAlert.Result) {
return nil
}
methods := strings.Split(alert.Method, ",")
for _, m := range methods {
@@ -126,7 +129,7 @@ func sendAlert(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.Push
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param))
params := alertUtil.CreateTaskAlertParams(pushAlert)
alertDetail := alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, constant.Bark)
alertRule := alertUtil.ProcessAlertRule(alert)
create.AlertRule = alertRule
+10 -2
View File
@@ -97,7 +97,7 @@ func (s sftpClient) Upload(ctx context.Context, src, target string) (bool, error
}()
defer close(done)
client, err := sftp.NewClient(sshClient)
client, err := sftp.NewClient(sshClient, sftp.UseConcurrentWrites(true))
if err != nil {
return false, err
}
@@ -127,7 +127,15 @@ func (s sftpClient) Upload(ctx context.Context, src, target string) (bool, error
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
// ReadFrom uses the pipelined write path of pkg/sftp. A plain io.Copy would
// pick os.File.WriteTo (the source is an *os.File), which issues one
// synchronous SFTP WRITE per packet and waits for the reply every time, so
// the throughput is bounded by packetSize/RTT.
written, err := dstFile.ReadFrom(srcFile)
if err != nil {
// A failed concurrent write may leave holes in the remote file, so cut
// it back to the number of bytes actually written.
_ = dstFile.Truncate(written)
if ctxErr := ctx.Err(); ctxErr != nil {
return false, ctxErr
}
+108
View File
@@ -0,0 +1,108 @@
package docker
import (
"context"
"sort"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
)
type NetworkCleanupClient interface {
NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error)
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
NetworkInspect(context.Context, string, network.InspectOptions) (network.Inspect, error)
NetworkRemove(context.Context, string) error
}
func CleanUnusedNetworks(ctx context.Context, cli NetworkCleanupClient, onResult ...func(string, dto.NetworkCleanupItem)) (*dto.NetworkCleanupReport, error) {
networks, err := cli.NetworkList(ctx, network.ListOptions{})
if err != nil {
return nil, err
}
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return nil, err
}
used := make(map[string]bool)
for _, c := range containers {
if c.NetworkSettings == nil {
continue
}
for name, endpoint := range c.NetworkSettings.Networks {
used[name] = true
if endpoint != nil {
used[endpoint.NetworkID] = true
}
}
}
report := &dto.NetworkCleanupReport{Deleted: []dto.NetworkCleanupItem{}, Skipped: []dto.NetworkCleanupItem{}, Failed: []dto.NetworkCleanupItem{}}
record := func(status string, item dto.NetworkCleanupItem) {
switch status {
case "deleted":
report.Deleted = append(report.Deleted, item)
case "skipped":
report.Skipped = append(report.Skipped, item)
case "failed":
report.Failed = append(report.Failed, item)
}
for _, notify := range onResult {
if notify != nil {
notify(status, item)
}
}
}
sort.Slice(networks, func(i, j int) bool { return networks[i].Name < networks[j].Name })
for _, n := range networks {
if err := ctx.Err(); err != nil {
return report, err
}
item := dto.NetworkCleanupItem{ID: n.ID, Name: n.Name}
switch {
case n.Name == "none" || n.Name == "host" || n.Name == "bridge" || n.Name == "1panel-network":
item.Reason = "protected"
case n.Scope != "local" || n.Ingress || n.ConfigOnly:
item.Reason = "unsupported_network"
case used[n.Name] || used[n.ID]:
item.Reason = "container_connected"
}
if item.Reason != "" {
record("skipped", item)
continue
}
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
if err != nil {
if errdefs.IsNotFound(err) {
item.Reason = "already_removed"
record("skipped", item)
} else {
item.Reason = "inspect_failed"
record("failed", item)
}
continue
}
if len(inspected.Containers) > 0 {
item.Reason = "container_connected"
record("skipped", item)
continue
}
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
switch {
case errdefs.IsNotFound(err):
item.Reason = "already_removed"
record("skipped", item)
case errdefs.IsConflict(err):
item.Reason = "network_in_use"
record("skipped", item)
default:
item.Reason = "remove_failed"
record("failed", item)
}
continue
}
record("deleted", item)
}
return report, nil
}
@@ -0,0 +1,72 @@
package docker
import (
"context"
"errors"
"reflect"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
)
type cleanupFake struct {
networks []network.Inspect
containers []container.Summary
inspected map[string]network.Inspect
inspectErrors, removeErrors map[string]error
listError error
removed []string
}
func (f *cleanupFake) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
return f.networks, nil
}
func (f *cleanupFake) ContainerList(_ context.Context, o container.ListOptions) ([]container.Summary, error) {
if !o.All {
panic("must include stopped containers")
}
return f.containers, f.listError
}
func (f *cleanupFake) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
return f.inspected[id], f.inspectErrors[id]
}
func (f *cleanupFake) NetworkRemove(_ context.Context, id string) error {
f.removed = append(f.removed, id)
return f.removeErrors[id]
}
func TestNetworkCleanupProtectsAndReports(t *testing.T) {
f := &cleanupFake{inspected: map[string]network.Inspect{"attached": {Containers: map[string]network.EndpointResource{"container": {}}}}, inspectErrors: map[string]error{"unknown": errors.New("inspect failure")}, removeErrors: map[string]error{"race": errdefs.Conflict(errors.New("has active endpoints")), "gone": errdefs.NotFound(errors.New("gone")), "failed": errors.New("denied")}}
for _, name := range []string{"none", "host", "bridge", "1panel-network", "configured", "attached", "stopped", "free", "unknown", "race", "gone", "failed"} {
f.networks = append(f.networks, network.Inspect{ID: name, Name: name, Scope: "local"})
}
// Compose ownership labels do not protect an otherwise unused network.
for i := range f.networks {
if f.networks[i].Name == "configured" {
f.networks[i].Labels = map[string]string{"com.docker.compose.project": "demo", "com.docker.compose.network": "default"}
}
}
f.containers = []container.Summary{{State: "exited", NetworkSettings: &container.NetworkSettingsSummary{Networks: map[string]*network.EndpointSettings{"stopped": {NetworkID: "stopped"}}}}}
report, err := CleanUnusedNetworks(context.Background(), f)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(f.removed, []string{"configured", "failed", "free", "gone", "race"}) {
t.Fatal(f.removed)
}
if len(report.Deleted) != 2 || report.Deleted[0].Name != "configured" || report.Deleted[1].Name != "free" || len(report.Failed) != 2 || len(report.Skipped) != 8 {
t.Fatalf("%+v", report)
}
}
func TestNetworkCleanupDiscoveryFailureDeletesNothing(t *testing.T) {
f := &cleanupFake{networks: []network.Inspect{{ID: "free", Name: "free", Scope: "local"}}, listError: errors.New("cannot list containers")}
if _, err := CleanUnusedNetworks(context.Background(), f); err == nil {
t.Fatal("expected error")
}
if len(f.removed) > 0 {
t.Fatal(f.removed)
}
}
+4
View File
@@ -6,6 +6,8 @@ import (
)
const (
VLLMMetricSamplePattern = `^([a-zA-Z_:][a-zA-Z0-9_:]*)(\{(?:[^"\\]|"(?:\\.|[^"\\])*")*\})?\s+(\S+)(?:\s+.*)?$`
VLLMMetricLabelPattern = `([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("(?:[^"\\]|\\.)*")`
NumberAlphaPattern = `(\d+)([A-Za-z]+)`
ComposeDisallowedCharsPattern = `[^a-z0-9_-]+`
ComposeNamePattern = `^[a-z0-9][a-z0-9_-]{0,255}$`
@@ -61,6 +63,8 @@ var regexMap = make(map[string]*regexp.Regexp)
func Init() {
patterns := []string{
VLLMMetricSamplePattern,
VLLMMetricLabelPattern,
NumberAlphaPattern,
ComposeDisallowedCharsPattern,
ComposeNamePattern,
+92 -1
View File
@@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"syscall"
"time"
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/app/model"
@@ -25,6 +26,7 @@ import (
"github.com/1Panel-dev/1Panel/core/utils/req_helper"
upgradeUtil "github.com/1Panel-dev/1Panel/core/utils/upgrade"
"github.com/1Panel-dev/1Panel/core/utils/xpack"
"golang.org/x/net/html"
)
type serviceInfo struct {
@@ -299,7 +301,7 @@ func (u *UpgradeService) LoadRelease() ([]dto.ReleasesNotes, error) {
docSource, _ := settingRepo.GetValueByKey("DocSource")
lang, _ := settingRepo.GetValueByKey("Language")
var notes []dto.ReleasesNotes
url := "https://1panel.cn/docs/v2/search/search_index.json"
url := "https://docs.fit2cloud.com/1panel/changelog/"
useIntlDocs := false
lang = strings.ToLower(strings.TrimSpace(lang))
if docSource == "withByRegion" {
@@ -315,6 +317,12 @@ func (u *UpgradeService) LoadRelease() ([]dto.ReleasesNotes, error) {
return notes, err
}
defer resp.Body.Close()
if !useIntlDocs {
if resp.StatusCode != http.StatusOK {
return notes, fmt.Errorf("load release notes failed: HTTP %d", resp.StatusCode)
}
return parseReleaseHTML(resp.Body)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return notes, err
@@ -336,6 +344,89 @@ func (u *UpgradeService) LoadRelease() ([]dto.ReleasesNotes, error) {
return notes, nil
}
func parseReleaseHTML(reader io.Reader) ([]dto.ReleasesNotes, error) {
doc, err := html.Parse(reader)
if err != nil {
return nil, err
}
var article *html.Node
for node := range doc.Descendants() {
if node.Type == html.ElementNode && node.Data == "article" {
article = node
break
}
}
if article == nil {
return nil, fmt.Errorf("release notes article not found")
}
textContent := func(node *html.Node) string {
var text strings.Builder
for child := range node.Descendants() {
if child.Type == html.TextNode {
text.WriteString(child.Data)
}
}
return strings.TrimSpace(strings.ReplaceAll(text.String(), "\u200b", ""))
}
var notes []dto.ReleasesNotes
for heading := range article.Descendants() {
if heading.Type != html.ElementNode || heading.Data != "h3" {
continue
}
version := textContent(heading)
if !strings.HasPrefix(version, "v") {
continue
}
item := dto.ReleasesNotes{Version: version}
var content strings.Builder
section := ""
for node := heading.NextSibling; node != nil; node = node.NextSibling {
if node.Type != html.ElementNode {
continue
}
if node.Data == "h1" || node.Data == "h2" || node.Data == "h3" {
break
}
if item.CreatedAt == "" {
date := textContent(node)
if _, err := time.Parse("2006年1月2日", date); node.Data != "p" || err != nil {
return nil, fmt.Errorf("release date not found for %s", version)
}
item.CreatedAt = date
continue
}
if node.Data == "p" {
section = textContent(node)
}
for child := range node.Descendants() {
if child.Type != html.ElementNode || child.Data != "li" {
continue
}
switch section {
case "新增功能":
item.NewCount++
case "功能优化":
item.OptimizationCount++
case "问题修复":
item.FixCount++
}
}
if err := html.Render(&content, node); err != nil {
return nil, err
}
}
if item.CreatedAt == "" || content.Len() == 0 {
return nil, fmt.Errorf("release notes content not found for %s", version)
}
item.Content = content.String()
notes = append(notes, item)
}
if len(notes) == 0 {
return nil, fmt.Errorf("release notes versions not found")
}
return notes, nil
}
func analyzeDoc(version, content string) dto.ReleasesNotes {
var item dto.ReleasesNotes
parts := strings.Split(content, "<p>")
+31 -4
View File
@@ -644,6 +644,13 @@
"formatZH": "创建容器网络 name",
"formatEN": "create container network [name]"
},
"/containers/network/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理未使用的容器网络",
"formatEN": "Clean unused container networks"
},
"/containers/network/del": {
"bodyKeys": [
"names"
@@ -2486,6 +2493,17 @@
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/core/xpack/vms/disks/resize": {
"bodyKeys": [
"name",
"target",
"sizeGiB"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
},
"/core/xpack/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
@@ -4028,12 +4046,21 @@
},
"/runtimes/operate": {
"bodyKeys": [
"id"
"ID"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作运行环境 [id]",
"formatEN": "Operate runtime [id]"
"beforeFunctions": [
{
"input_column": "id",
"input_value": "ID",
"isList": false,
"db": "runtimes",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "操作运行环境 [name]",
"formatEN": "Operate runtime [name]"
},
"/runtimes/php/config": {
"bodyKeys": [
+5 -5
View File
@@ -1,6 +1,6 @@
module github.com/1Panel-dev/1Panel/core
go 1.26.1
go 1.26.6
require (
github.com/beevik/etree v1.7.0
@@ -39,8 +39,8 @@ require (
github.com/swaggo/files/v2 v2.0.2
github.com/swaggo/swag v1.16.6
github.com/xlzd/gotp v0.1.0
golang.org/x/crypto v0.55.0
golang.org/x/image v0.41.0
golang.org/x/crypto v0.56.0
golang.org/x/image v0.45.0
golang.org/x/net v0.58.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.47.0
@@ -129,9 +129,9 @@ require (
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/arch v0.26.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/mod v0.40.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/tools v0.48.0 // indirect
golang.org/x/tools v0.49.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
+8 -10
View File
@@ -124,8 +124,6 @@ github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -304,13 +302,13 @@ golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4=
golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
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/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
@@ -335,8 +333,8 @@ golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
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/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "Virtual machine network does not exist: {{ .detail }}"
ErrVMInvalidPath: "Invalid virtual machine path: {{ .detail }}"
ErrVMInvalidInput: "Invalid virtual machine parameter: {{ .detail }}"
ErrVMCommandFailed: "Virtual machine command failed: {{ .err }}"
ErrVMDiskResizeBlocked: "The disk does not meet expansion requirements or is referenced by another device. Check the disk status."
ErrVMDiskReferenced: "This virtual disk is used by another virtual machine or device and cannot be expanded yet."
ErrVMDiskReferenceCheck: "Cannot complete virtual disk reference checks. Expansion is unavailable; check the disks of other virtual machines."
ErrVMSnapshotMetadataSync: "The snapshot was restored, but disk information could not be synced. Refresh and check; do not restore the snapshot again."
VMCreateTask: "Create virtual machine [{{ .name }}]"
VMCreateFromTemplateTask: "Create virtual machine [{{ .name }}] from template"
VMUpdateTask: "Update virtual machine [{{ .name }}]"
VMDiskResizeTask: "Expand disk [{{ .disk }}] of virtual machine [{{ .name }}]"
VMRenameTask: "Rename virtual machine [{{ .name }}] to [{{ .newName }}]"
VMDeleteTask: "Delete virtual machine [{{ .name }}]"
VMTemplateCreateTask: "Save virtual machine [{{ .name }}] as template [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "La red de la máquina virtual no existe: {{ .detail }}"
ErrVMInvalidPath: "Ruta de máquina virtual no válida: {{ .detail }}"
ErrVMInvalidInput: "Parámetro de máquina virtual no válido: {{ .detail }}"
ErrVMCommandFailed: "Error al ejecutar el comando de la máquina virtual: {{ .err }}"
ErrVMDiskResizeBlocked: "El disco no cumple los requisitos de ampliación o está referenciado por otro dispositivo. Compruebe su estado."
ErrVMDiskReferenced: "Otro equipo virtual o dispositivo utiliza este disco virtual; la ampliación aún no se admite."
ErrVMDiskReferenceCheck: "No se pueden comprobar las referencias al disco virtual. La ampliación no está disponible; revise los discos de otras máquinas virtuales."
ErrVMSnapshotMetadataSync: "La instantánea se restauró, pero falló la sincronización de los datos del disco. Actualice y compruebe; no restaure de nuevo la instantánea."
VMCreateTask: "Crear máquina virtual [{{ .name }}]"
VMCreateFromTemplateTask: "Crear máquina virtual [{{ .name }}] desde plantilla"
VMUpdateTask: "Actualizar máquina virtual [{{ .name }}]"
VMDiskResizeTask: "Ampliar disco [{{ .disk }}] de la máquina virtual [{{ .name }}]"
VMRenameTask: "Renombrar máquina virtual [{{ .name }}] a [{{ .newName }}]"
VMDeleteTask: "Eliminar máquina virtual [{{ .name }}]"
VMTemplateCreateTask: "Guardar máquina virtual [{{ .name }}] como plantilla [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "شبکه ماشین مجازی وجود ندارد: {{ .d
ErrVMInvalidPath: "مسیر ماشین مجازی نامعتبر است: {{ .detail }}"
ErrVMInvalidInput: "پارامتر ماشین مجازی نامعتبر است: {{ .detail }}"
ErrVMCommandFailed: "اجرای دستور ماشین مجازی ناموفق بود: {{ .err }}"
ErrVMDiskResizeBlocked: "دیسک شرایط افزایش ظرفیت را ندارد یا دستگاه دیگری به آن ارجاع می‌دهد. وضعیت دیسک را بررسی کنید."
ErrVMDiskReferenced: "این دیسک مجازی توسط ماشین مجازی یا دستگاه دیگری استفاده می‌شود؛ افزایش ظرفیت فعلاً پشتیبانی نمی‌شود."
ErrVMDiskReferenceCheck: "بررسی ارجاع‌های دیسک مجازی کامل نشد. افزایش ظرفیت ممکن نیست؛ دیسک‌های سایر ماشین‌های مجازی را بررسی کنید."
ErrVMSnapshotMetadataSync: "اسنپ‌شات بازیابی شد، اما همگام‌سازی اطلاعات دیسک ناموفق بود. تازه‌سازی و بررسی کنید؛ نیازی به بازیابی دوباره اسنپ‌شات نیست."
VMCreateTask: "ایجاد ماشین مجازی [{{ .name }}]"
VMCreateFromTemplateTask: "ایجاد ماشین مجازی [{{ .name }}] از قالب"
VMUpdateTask: "به‌روزرسانی ماشین مجازی [{{ .name }}]"
VMDiskResizeTask: "افزایش ظرفیت دیسک [{{ .disk }}] ماشین مجازی [{{ .name }}]"
VMRenameTask: "تغییر نام ماشین مجازی [{{ .name }}] به [{{ .newName }}]"
VMDeleteTask: "حذف ماشین مجازی [{{ .name }}]"
VMTemplateCreateTask: "ذخیره ماشین مجازی [{{ .name }}] به عنوان قالب [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "仮想マシンネットワークが存在しません: {
ErrVMInvalidPath: "仮想マシンのパスが無効です: {{ .detail }}"
ErrVMInvalidInput: "仮想マシンのパラメータが無効です: {{ .detail }}"
ErrVMCommandFailed: "仮想マシンコマンドの実行に失敗しました: {{ .err }}"
ErrVMDiskResizeBlocked: "ディスクが拡張条件を満たしていないか、他のデバイスから参照されています。ディスクの状態を確認してください。"
ErrVMDiskReferenced: "この仮想ディスクは他の仮想マシンまたはデバイスで使用されているため、現在は拡張できません。"
ErrVMDiskReferenceCheck: "仮想ディスクの参照を確認できないため拡張できません。他の仮想マシンのディスク状態を確認してください。"
ErrVMSnapshotMetadataSync: "スナップショットは復元されましたが、ディスク情報の同期に失敗しました。更新して確認してください。再度復元する必要はありません。"
VMCreateTask: "仮想マシン [{{ .name }}] を作成"
VMCreateFromTemplateTask: "テンプレートから仮想マシン [{{ .name }}] を作成"
VMUpdateTask: "仮想マシン [{{ .name }}] を更新"
VMDiskResizeTask: "仮想マシン [{{ .name }}] のディスク [{{ .disk }}] を拡張"
VMRenameTask: "仮想マシン [{{ .name }}] を [{{ .newName }}] にリネーム"
VMDeleteTask: "仮想マシン [{{ .name }}] を削除"
VMTemplateCreateTask: "仮想マシン [{{ .name }}] をテンプレート [{{ .template }}] として保存"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "가상 머신 네트워크가 존재하지 않습니다:
ErrVMInvalidPath: "가상 머신 경로가 유효하지 않습니다: {{ .detail }}"
ErrVMInvalidInput: "가상 머신 매개변수가 유효하지 않습니다: {{ .detail }}"
ErrVMCommandFailed: "가상 머신 명령 실행 실패: {{ .err }}"
ErrVMDiskResizeBlocked: "디스크가 확장 조건을 충족하지 않거나 다른 장치에서 참조하고 있습니다. 디스크 상태를 확인하세요."
ErrVMDiskReferenced: "다른 가상 머신 또는 장치가 이 가상 디스크를 사용 중이므로 현재 확장할 수 없습니다."
ErrVMDiskReferenceCheck: "가상 디스크 참조를 확인할 수 없어 확장할 수 없습니다. 다른 가상 머신의 디스크 상태를 확인하세요."
ErrVMSnapshotMetadataSync: "스냅샷은 복원되었지만 디스크 정보 동기화에 실패했습니다. 새로 고쳐 확인하세요. 스냅샷을 다시 복원할 필요는 없습니다."
VMCreateTask: "가상 머신 [{{ .name }}] 생성"
VMCreateFromTemplateTask: "템플릿에서 가상 머신 [{{ .name }}] 생성"
VMUpdateTask: "가상 머신 [{{ .name }}] 업데이트"
VMDiskResizeTask: "가상 머신 [{{ .name }}]의 디스크 [{{ .disk }}] 확장"
VMRenameTask: "가상 머신 [{{ .name }}]을(를) [{{ .newName }}](으)로 이름 변경"
VMDeleteTask: "가상 머신 [{{ .name }}] 삭제"
VMTemplateCreateTask: "가상 머신 [{{ .name }}]을(를) 템플릿 [{{ .template }}](으)로 저장"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "ບໍ່ມີເຄືອຂ່າຍເຄື່ອງ
ErrVMInvalidPath: "ເສັ້ນທາງເຄື່ອງສະເໝືອນບໍ່ຖືກຕ້ອງ: {{ .detail }}"
ErrVMInvalidInput: "ພາລາມິເຕີເຄື່ອງສະເໝືອນບໍ່ຖືກຕ້ອງ: {{ .detail }}"
ErrVMCommandFailed: "ຄຳສັ່ງເຄື່ອງສະເໝືອນລົ້ມເຫຼວ: {{ .err }}"
ErrVMDiskResizeBlocked: "ດິສກ໌ບໍ່ຕອບສະໜອງເງື່ອນໄຂການຂະຫຍາຍ ຫຼື ຖືກອ້າງອີງໂດຍອຸປະກອນອື່ນ. ກະລຸນາກວດສອບສະຖານະດິສກ໌."
ErrVMDiskReferenced: "ດິສກ໌ເສມືອນນີ້ຖືກໃຊ້ໂດຍເຄື່ອງເສມືອນ ຫຼື ອຸປະກອນອື່ນ; ຍັງບໍ່ຮອງຮັບການຂະຫຍາຍ."
ErrVMDiskReferenceCheck: "ບໍ່ສາມາດກວດສອບການອ້າງອີງດິສກ໌ເສມືອນໄດ້. ຍັງຂະຫຍາຍບໍ່ໄດ້; ກະລຸນາກວດສອບດິສກ໌ຂອງເຄື່ອງເສມືອນອື່ນ."
ErrVMSnapshotMetadataSync: "ກູ້ຄືນສະແນັບຊັອດແລ້ວ ແຕ່ຊິງຂໍ້ມູນດິສກ໌ບໍ່ສຳເລັດ. ກະລຸນາໂຫຼດໃໝ່ ແລະ ກວດສອບ; ບໍ່ຈຳເປັນຕ້ອງກູ້ຄືນສະແນັບຊັອດອີກ."
VMCreateTask: "ສ້າງເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMCreateFromTemplateTask: "ສ້າງເຄື່ອງສະເໝືອນ [{{ .name }}] ຈາກແມ່ແບບ"
VMUpdateTask: "ອັບເດດເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMDiskResizeTask: "ຂະຫຍາຍດິສກ໌ [{{ .disk }}] ຂອງເຄື່ອງເສມືອນ [{{ .name }}]"
VMRenameTask: "ປ່ຽນຊື່ເຄື່ອງສະເໝືອນ [{{ .name }}] ເປັນ [{{ .newName }}]"
VMDeleteTask: "ລຶບເຄື່ອງສະເໝືອນ [{{ .name }}]"
VMTemplateCreateTask: "ບັນທຶກເຄື່ອງສະເໝືອນ [{{ .name }}] ເປັນແມ່ແບບ [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "Rangkaian mesin maya tidak wujud: {{ .detail }}"
ErrVMInvalidPath: "Laluan mesin maya tidak sah: {{ .detail }}"
ErrVMInvalidInput: "Parameter mesin maya tidak sah: {{ .detail }}"
ErrVMCommandFailed: "Arahan mesin maya gagal: {{ .err }}"
ErrVMDiskResizeBlocked: "Cakera tidak memenuhi syarat pembesaran atau dirujuk oleh peranti lain. Semak status cakera."
ErrVMDiskReferenced: "Cakera maya ini digunakan oleh mesin maya atau peranti lain; pembesaran belum disokong."
ErrVMDiskReferenceCheck: "Rujukan cakera maya tidak dapat disahkan. Pembesaran tidak tersedia; semak cakera mesin maya lain."
ErrVMSnapshotMetadataSync: "Syot kilat telah dipulihkan, tetapi maklumat cakera gagal disegerakkan. Muat semula dan semak; tidak perlu memulihkan syot kilat sekali lagi."
VMCreateTask: "Cipta mesin maya [{{ .name }}]"
VMCreateFromTemplateTask: "Cipta mesin maya [{{ .name }}] daripada templat"
VMUpdateTask: "Kemas kini mesin maya [{{ .name }}]"
VMDiskResizeTask: "Besarkan cakera [{{ .disk }}] mesin maya [{{ .name }}]"
VMRenameTask: "Namakan semula mesin maya [{{ .name }}] kepada [{{ .newName }}]"
VMDeleteTask: "Padam mesin maya [{{ .name }}]"
VMTemplateCreateTask: "Simpan mesin maya [{{ .name }}] sebagai templat [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "Rede da máquina virtual não existe: {{ .detail }}"
ErrVMInvalidPath: "Caminho da máquina virtual inválido: {{ .detail }}"
ErrVMInvalidInput: "Parâmetro da máquina virtual inválido: {{ .detail }}"
ErrVMCommandFailed: "Falha no comando da máquina virtual: {{ .err }}"
ErrVMDiskResizeBlocked: "O disco não atende aos requisitos de expansão ou é referenciado por outro dispositivo. Verifique seu estado."
ErrVMDiskReferenced: "Este disco virtual é usado por outra máquina virtual ou dispositivo e ainda não pode ser expandido."
ErrVMDiskReferenceCheck: "Não foi possível verificar as referências ao disco virtual. A expansão está indisponível; verifique os discos das outras máquinas virtuais."
ErrVMSnapshotMetadataSync: "O snapshot foi restaurado, mas a sincronização dos dados do disco falhou. Atualize e verifique; não restaure o snapshot novamente."
VMCreateTask: "Criar máquina virtual [{{ .name }}]"
VMCreateFromTemplateTask: "Criar máquina virtual [{{ .name }}] a partir do modelo"
VMUpdateTask: "Atualizar máquina virtual [{{ .name }}]"
VMDiskResizeTask: "Expandir disco [{{ .disk }}] da máquina virtual [{{ .name }}]"
VMRenameTask: "Renomear máquina virtual [{{ .name }}] para [{{ .newName }}]"
VMDeleteTask: "Excluir máquina virtual [{{ .name }}]"
VMTemplateCreateTask: "Salvar máquina virtual [{{ .name }}] como modelo [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "Сеть виртуальной машины не сущ
ErrVMInvalidPath: "Недопустимый путь виртуальной машины: {{ .detail }}"
ErrVMInvalidInput: "Недопустимые параметры виртуальной машины: {{ .detail }}"
ErrVMCommandFailed: "Ошибка выполнения команды виртуальной машины: {{ .err }}"
ErrVMDiskResizeBlocked: "Диск не соответствует условиям расширения или используется другим устройством. Проверьте состояние диска."
ErrVMDiskReferenced: "Этот виртуальный диск используется другой виртуальной машиной или устройством; расширение пока недоступно."
ErrVMDiskReferenceCheck: "Не удалось проверить ссылки на виртуальный диск. Расширение недоступно; проверьте диски других виртуальных машин."
ErrVMSnapshotMetadataSync: "Снимок восстановлен, но синхронизация данных диска не удалась. Обновите данные и проверьте их; повторно восстанавливать снимок не нужно."
VMCreateTask: "Создать виртуальную машину [{{ .name }}]"
VMCreateFromTemplateTask: "Создать виртуальную машину [{{ .name }}] из шаблона"
VMUpdateTask: "Обновить виртуальную машину [{{ .name }}]"
VMDiskResizeTask: "Расширить диск [{{ .disk }}] виртуальной машины [{{ .name }}]"
VMRenameTask: "Переименовать виртуальную машину [{{ .name }}] в [{{ .newName }}]"
VMDeleteTask: "Удалить виртуальную машину [{{ .name }}]"
VMTemplateCreateTask: "Сохранить виртуальную машину [{{ .name }}] как шаблон [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "Sanal makine ağı mevcut değil: {{ .detail }}"
ErrVMInvalidPath: "Geçersiz sanal makine yolu: {{ .detail }}"
ErrVMInvalidInput: "Geçersiz sanal makine parametresi: {{ .detail }}"
ErrVMCommandFailed: "Sanal makine komutu başarısız: {{ .err }}"
ErrVMDiskResizeBlocked: "Disk genişletme koşullarını karşılamıyor veya başka bir aygıt tarafından kullanılıyor. Disk durumunu kontrol edin."
ErrVMDiskReferenced: "Bu sanal disk başka bir sanal makine veya aygıt tarafından kullanılıyor; genişletme henüz desteklenmiyor."
ErrVMDiskReferenceCheck: "Sanal disk başvuruları doğrulanamadı. Genişletme kullanılamıyor; diğer sanal makinelerin disklerini kontrol edin."
ErrVMSnapshotMetadataSync: "Anlık görüntü geri yüklendi ancak disk bilgileri eşitlenemedi. Yenileyip kontrol edin; anlık görüntüyü tekrar geri yüklemeyin."
VMCreateTask: "Sanal makine [{{ .name }}] oluştur"
VMCreateFromTemplateTask: "Şablondan sanal makine [{{ .name }}] oluştur"
VMUpdateTask: "Sanal makine [{{ .name }}] güncelle"
VMDiskResizeTask: "[{{ .name }}] sanal makinesinin [{{ .disk }}] diskini genişlet"
VMRenameTask: "Sanal makine [{{ .name }}] adını [{{ .newName }}] olarak değiştir"
VMDeleteTask: "Sanal makine [{{ .name }}] sil"
VMTemplateCreateTask: "Sanal makine [{{ .name }}] öğesini şablon [{{ .template }}] olarak kaydet"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "虛擬機網路不存在: {{ .detail }}"
ErrVMInvalidPath: "虛擬機路徑非法: {{ .detail }}"
ErrVMInvalidInput: "虛擬機參數非法: {{ .detail }}"
ErrVMCommandFailed: "虛擬機指令執行失敗: {{ .err }}"
ErrVMDiskResizeBlocked: "磁碟不符合擴容條件,或被其他裝置引用,請檢查磁碟狀態"
ErrVMDiskReferenced: "此虛擬硬碟被其他虛擬機或裝置使用,暫不支援擴容"
ErrVMDiskReferenceCheck: "無法完成虛擬硬碟引用檢查,暫不支援擴容,請檢查其他虛擬機的磁碟狀態"
ErrVMSnapshotMetadataSync: "快照已還原,但磁碟資訊同步失敗,請重新整理檢查,無需重複還原快照"
VMCreateTask: "建立虛擬機 [{{ .name }}]"
VMCreateFromTemplateTask: "從範本建立虛擬機 [{{ .name }}]"
VMUpdateTask: "更新虛擬機 [{{ .name }}]"
VMDiskResizeTask: "擴容虛擬機 [{{ .name }}] 磁碟 [{{ .disk }}]"
VMRenameTask: "重新命名虛擬機 [{{ .name }}] 為 [{{ .newName }}]"
VMDeleteTask: "刪除虛擬機 [{{ .name }}]"
VMTemplateCreateTask: "轉存虛擬機 [{{ .name }}] 為範本 [{{ .template }}]"
+5
View File
@@ -152,9 +152,14 @@ ErrVMNetworkNotFound: "虚拟机网络不存在: {{ .detail }}"
ErrVMInvalidPath: "虚拟机路径非法: {{ .detail }}"
ErrVMInvalidInput: "虚拟机参数非法: {{ .detail }}"
ErrVMCommandFailed: "虚拟机命令执行失败: {{ .err }}"
ErrVMDiskResizeBlocked: "磁盘不符合扩容条件,或被其他设备引用,请检查磁盘状态"
ErrVMDiskReferenced: "该虚拟硬盘被其他虚拟机或设备使用,暂不支持扩容"
ErrVMDiskReferenceCheck: "无法完成虚拟硬盘引用检查,暂不支持扩容,请检查其他虚拟机的磁盘状态"
ErrVMSnapshotMetadataSync: "快照已恢复,但磁盘信息同步失败,请刷新检查,无需重复恢复快照"
VMCreateTask: "创建虚拟机 [{{ .name }}]"
VMCreateFromTemplateTask: "从模板创建虚拟机 [{{ .name }}]"
VMUpdateTask: "更新虚拟机 [{{ .name }}]"
VMDiskResizeTask: "扩容虚拟机 [{{ .name }}] 磁盘 [{{ .disk }}]"
VMRenameTask: "重命名虚拟机 [{{ .name }}] 为 [{{ .newName }}]"
VMDeleteTask: "删除虚拟机 [{{ .name }}]"
VMTemplateCreateTask: "转存虚拟机 [{{ .name }}] 为模板 [{{ .template }}]"
+1
View File
@@ -87,6 +87,7 @@ func Routers() *gin.Engine {
}
Router.Use(middleware.FrontendFallback())
Router.Use(middleware.WebSocketOriginGuard())
Router.Use(middleware.OperationLog())
Router.Use(middleware.GlobalLoading())
Router.Use(xpack.AuthProvider.CoreAPIAuthMiddleware())
+5
View File
@@ -27,6 +27,11 @@ var demoAllowedRoutes = map[demoRoute]struct{}{
{http.MethodPost, "/api/v2/toolbox/clam/base"}: {},
{http.MethodPost, "/api/v2/backups/record/size"}: {},
{http.MethodPost, "/api/v2/hosts/firewall/base"}: {},
{http.MethodPost, "/api/v2/hosts/firewall/forward/base"}: {},
{http.MethodPost, "/api/v2/hosts/firewall/rules/native/detail"}: {},
{http.MethodPost, "/api/v2/hosts/firewall/rules/sync/preview"}: {},
{http.MethodPost, "/api/v2/core/auth/login"}: {},
{http.MethodPost, "/api/v2/core/logs/login"}: {},
{http.MethodPost, "/api/v2/core/logs/operation"}: {},
+41
View File
@@ -0,0 +1,41 @@
package middleware
import (
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
func WebSocketOriginGuard() gin.HandlerFunc {
return func(c *gin.Context) {
if !websocket.IsWebSocketUpgrade(c.Request) {
c.Next()
return
}
if site := c.GetHeader("Sec-Fetch-Site"); site != "" && site != "same-origin" {
c.AbortWithStatus(http.StatusForbidden)
return
}
origins := c.Request.Header.Values("Origin")
if len(origins) == 0 {
c.Next()
return
}
if len(origins) != 1 {
c.AbortWithStatus(http.StatusForbidden)
return
}
origin, err := url.Parse(origins[0])
if err != nil || (origin.Scheme != "http" && origin.Scheme != "https") ||
origin.Host == "" || !strings.EqualFold(origin.Host, c.Request.Host) ||
origin.User != nil || origin.Path != "" || origin.RawQuery != "" || origin.ForceQuery || origin.Fragment != "" {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}
}
+2 -2
View File
@@ -53,11 +53,11 @@
$ bash -c "$(curl -sSL https://resource.fit2cloud.com/1panel/package/v2/quick_start.sh)"
如果是用于离线环境,推荐使用 [安装包方式](https://1panel.cn/docs/v2/installation/enterprise_installation/) 进行安装部署。
如果是用于离线环境,推荐使用 [安装包方式](https://docs.fit2cloud.com/1panel/installation/enterprise-installation/) 进行安装部署。
**学习资料**
- [在线文档](https://1panel.cn/docs/)
- [在线文档](https://docs.fit2cloud.com/1panel/)
- [社区论坛](https://bbs.fit2cloud.com/c/1p/7)
## 版本对比
+2 -2
View File
@@ -55,11 +55,11 @@
curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sudo bash quick_start.sh
```
如果是用於離線環境,推薦使用 [安裝包方式](https://1panel.cn/docs/installation/package_installation/) 進行安裝部署。
如果是用於離線環境,推薦使用 [安裝包方式](https://docs.fit2cloud.com/1panel/installation/enterprise-installation/) 進行安裝部署。
**學習資料**
- [線上文件](https://1panel.cn/docs/)
- [線上文件](https://docs.fit2cloud.com/1panel/)
- [社區論壇](https://bbs.fit2cloud.com/c/1p/7)
- [如何加入微信交流群?](https://bbs.fit2cloud.com/t/topic/2147)
+33
View File
@@ -0,0 +1,33 @@
# 批量清理未使用的容器网络
`POST /api/v2/containers/network/clean`,使用现有 1Panel 认证,不需要请求体。
这是异步接口,立即返回 `data.taskID`。实际结果在任务中心和任务日志查看,HTTP 请求结束不会取消任务。
规则:
- 永远保留 `none`、`host`、`bridge`、`1panel-network`。
- 保留所有容器(包括停止容器)引用的网络,删除前再通过 NetworkInspect 检查连接。
- 不检查 Compose 文件或应用配置引用;仅被这些配置引用、没有容器连接的普通网络也会被删除。
- 当前仅清理本地普通网络,跳过 Swarm、ingress 和 config-only 网络。
- Docker 删除时发现新连接会拒绝删除;不会强制断开容器。
成功响应示例:
```json
{"code":200,"data":{"taskID":"服务端生成的任务 UUID"}}
```
任务类型为 Container,操作为 TaskClean。网络页“清理网络”按钮调用此接口,拿到 taskID 后立即打开任务日志,并刷新任务中心。
每个网络处理完成后写入名称、ID 和结果;有容器连接或 Docker 返回占用冲突时,明确记录“未删除”。最后记录已删除、跳过和失败数量。
跳过在用网络属于正常结果;检查/删除失败会将任务标记为失败,已删除的网络不回滚。
日志调用链:
- `agent/app/service/container_network_cleanup.go`:NewTaskWithOps 创建任务,AddSubTask 注册清理步骤,Execute 执行任务,t.Log 写逐项日志。
- `agent/app/task/task.go`:任务持久化,维护状态,写入开始、结束标记;日志文件为 `<global.Dir.TaskDir>/Container/<taskID>.log`。
- `frontend/src/views/container/network/index.vue`:请求成功后调用 openTaskLog(taskID)。
- `frontend/src/components/log/task/index.vue`:openWithTaskID 打开日志弹窗。
- `frontend/src/components/log/file/index.vue`:通过 `/logs/tasks/read` 按行读取任务日志。
范围:不检查 Compose 文件或应用配置引用;仅清理本地普通网络。原 `/containers/prune` 接口仍保持原逻辑。
测试使用假 Docker 客户端和临时任务数据库,不执行真实网络删除。
+83 -309
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -57,18 +57,18 @@
"vue-codemirror": "^6.1.1",
"vue-demi": "^0.14.6",
"vue-i18n": "^11.4.4",
"vue-router": "^5.2.0"
"vue-router": "^5.3.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.3",
"@types/node": "^26.4.0",
"@types/node": "^26.6.3",
"@types/uuid": "^11.0.0",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.68.0",
"@vitejs/plugin-vue": "^6.0.7",
"@vitejs/plugin-vue-jsx": "^5.1.6",
"@vue/compiler-sfc": "^3.5.42",
"autoprefixer": "^10.5.4",
"autoprefixer": "^10.6.1",
"esbuild": "^0.28.2",
"eslint": "^10.8.0",
"eslint-config-prettier": "^10.1.8",
@@ -79,7 +79,7 @@
"postcss-html": "^2.0.0",
"prettier": "^3.9.6",
"rollup-plugin-visualizer": "^7.0.1",
"sass": "^1.103.1",
"sass": "^1.105.0",
"standard-version": "^9.5.0",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3",
@@ -88,7 +88,7 @@
"vite": "^8.2.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-eslint2": "^5.3.0",
"vite-svg-loader": "^5.1.2",
"vite-svg-loader": "^5.1.3",
"vue-eslint-parser": "^10.4.1",
"vue-tsc": "^3.3.9"
},
+91 -11
View File
@@ -22,6 +22,8 @@ export namespace AI {
}
export interface Info {
collectedAt?: string;
warnings?: string[];
cudaVersion: string;
driverVersion: string;
type: string;
@@ -31,6 +33,34 @@ export namespace AI {
xpu: XpuInfo['xpu'];
}
export interface GPU {
eccPending?: string;
eccErrors?: { scope: string; correctable: string; uncorrectable: string }[];
memoryActivity?: string;
encoderUtil?: string;
decoderUtil?: string;
jpegUtil?: string;
ofaUtil?: string;
mediaUtil?: string;
hotspotTemperature?: string;
fanRPM?: string;
mediaFrequency?: string;
uuid?: string;
driverVersion?: string;
architecture?: string;
frequency?: string;
memoryFrequency?: string;
memoryTemperature?: string;
memoryFree?: string;
memoryReserved?: string;
powerLimit?: string;
defaultPowerLimit?: string;
pcieGeneration?: string;
pcieMaxGeneration?: string;
pcieWidth?: string;
pcieMaxWidth?: string;
clockEvents?: string[];
processStatus?: string;
type: 'nvidia' | 'amd';
index: number;
productName: string;
@@ -52,6 +82,12 @@ export namespace AI {
processes: Process[];
}
export interface NPU {
aiCPUUtil?: string;
ctrlCPUUtil?: string;
ddrBandwidth?: string;
hbmBandwidth?: string;
processStatus?: string;
type: 'ascend';
index: number;
npuIndex: number;
@@ -73,18 +109,24 @@ export namespace AI {
processes: NPUProcess[];
}
export interface MonitorGPUSearch {
aggregation?: 'avg' | 'max';
deviceID?: string;
legacy?: boolean;
productName: string;
startTime: Date;
endTime: Date;
}
export interface MonitorGPUOptions {
supported: boolean;
gpuType: string;
options: Array<string>;
chartHide: Array<ChartHide>;
}
export interface ChartHide {
deviceID?: string;
legacy?: boolean;
productName: string;
type?: 'gpu' | 'xpu';
type?: 'gpu' | 'npu' | 'xpu';
process: boolean;
gpu: boolean;
memory: boolean;
@@ -94,17 +136,45 @@ export namespace AI {
speed: boolean;
}
export interface MonitorGPUData {
memoryActivity: Array<number | null>;
encoderUtil: Array<number | null>;
decoderUtil: Array<number | null>;
jpegUtil: Array<number | null>;
ofaUtil: Array<number | null>;
mediaUtil: Array<number | null>;
computeUtil: Array<number | null>;
copyUtil: Array<number | null>;
hotspotTemperature: Array<number | null>;
fanRPM: Array<number | null>;
aiCPUUtil: Array<number | null>;
ctrlCPUUtil: Array<number | null>;
ddrUsed: Array<number | null>;
ddrTotal: Array<number | null>;
hbmUsed: Array<number | null>;
hbmTotal: Array<number | null>;
ddrBandwidth: Array<number | null>;
hbmBandwidth: Array<number | null>;
memoryBandwidth: Array<number | null>;
mediaFrequency: Array<number | null>;
hugepagesUsed: Array<number | null>;
hugepagesTotal: Array<number | null>;
bucketSeconds: number;
sampleCount: number;
memoryTemperatureValue: Array<number | null>;
frequencyValue: Array<number | null>;
memoryFrequencyValue: Array<number | null>;
date: Array<Date>;
gpuValue: Array<number>;
temperatureValue: Array<number>;
powerTotal: Array<number>;
powerUsed: Array<number>;
powerPercent: Array<number>;
memoryTotal: Array<number>;
memoryUsed: Array<number>;
memoryPercent: Array<number>;
speedValue: Array<number>;
processCount: Array<number>;
gpuValue: Array<number | null>;
temperatureValue: Array<number | null>;
powerTotal: Array<number | null>;
powerUsed: Array<number | null>;
powerPercent: Array<number | null>;
memoryTotal: Array<number | null>;
memoryUsed: Array<number | null>;
memoryPercent: Array<number | null>;
speedValue: Array<number | null>;
processCount: Array<number | null>;
gpuProcesses: Array<Array<GPUProcess>>;
}
export interface GPUProcess {
@@ -132,12 +202,15 @@ export namespace AI {
}
interface Xpu {
tiles?: { tileID: number; stats: Stats }[];
processStatus?: string;
basic: Basic;
stats: Stats;
processes: XpuProcess[];
}
interface Basic {
uuid?: string;
deviceID: number;
deviceName: string;
vendorName: string;
@@ -148,6 +221,13 @@ export namespace AI {
}
interface Stats {
mediaUtil?: string;
computeUtil?: string;
copyUtil?: string;
mediaFrequency?: string;
memoryTemperature?: string;
memoryBandwidthUtil?: string;
power: string;
gpuUtil: string;
frequency: string;
+7 -1
View File
@@ -1,4 +1,5 @@
import { CommonModel, ReqPage } from '@/api/interface';
import type { CronjobAlertTriggerMode } from '@/utils/cronjob-alert';
export namespace Alert {
export interface AlertInfo extends CommonModel {
@@ -9,22 +10,25 @@ export namespace Alert {
interval: number;
method: string;
title: string;
taskName?: string;
project: string;
status: string;
sendCount: number;
sendMethod: string[];
advancedParams: string;
alertTriggerMode?: CronjobAlertTriggerMode;
createUser?: string;
updateUser?: string;
}
export interface AlertDetail {
type: string;
subType?: string;
licenseId: string;
title: string;
project: string;
method: string;
params: string;
params: { index: string; key: string; value: string }[] | string;
}
export interface AlertUpdateStatusReq {
@@ -74,6 +78,7 @@ export namespace Alert {
project: string;
status: string;
sendCount: number;
advancedParams?: string;
}
export interface AlertUpdateReq extends CommonModel {
@@ -86,6 +91,7 @@ export namespace Alert {
project: string;
status: string;
sendCount: number;
advancedParams?: string;
}
export interface DelReq {

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