mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
parent
ebd61ee109
commit
6f582671b0
4 changed files with 69 additions and 7 deletions
|
|
@ -188,6 +188,10 @@ CPU fallback points use host-capacity-normalized percent, while Docker metadata
|
|||
may expose runtime-native per-core CPU percent plus the host CPU count as raw
|
||||
diagnostic evidence. API consumers must not treat raw per-core CPU as the
|
||||
threshold, chart, or canonical resource CPU value.
|
||||
Proxmox legacy model CPU fields have the inverse compatibility shape:
|
||||
`models.Node.CPU`, `models.VM.CPU`, and `models.Container.CPU` remain 0..1
|
||||
ratios, but `/api/metrics-store/history` live fallback points must convert
|
||||
those ratios to 0..100 chart percentages before serialization.
|
||||
|
||||
Candidate import plans for PVE/PBS/PMG onboarding are API-contract consumers,
|
||||
not local UI estimates. Probe and Discovery candidates must build their
|
||||
|
|
@ -2459,8 +2463,10 @@ a new API state machine, queue contract, or verification-accounting field.
|
|||
and `POST /api/connections/probe` stay on one canonical payload shape
|
||||
instead of re-deriving state from per-type config stores in the frontend.
|
||||
State must remain a derived field sourced from in-memory scheduler health
|
||||
(`monitoring.Monitor.SchedulerHealth()`) plus agent `Host.LastSeen`; the
|
||||
endpoint must not introduce new persisted per-connection state. The probe
|
||||
(`monitoring.Monitor.SchedulerHealth()`) plus agent `Host.LastSeen`; for
|
||||
agent rows, `Host.LastSeen` is the monitoring-owned server receipt time,
|
||||
not the agent payload timestamp. The endpoint must not introduce new
|
||||
persisted per-connection state. The probe
|
||||
endpoint must remain admin-gated (`RequireAdmin` + `ScopeSettingsWrite`)
|
||||
to block unauthenticated SSRF against internal hosts. That same probe path
|
||||
must also validate user-supplied addresses before probing, reject metadata,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
|
@ -10802,6 +10803,57 @@ func TestContract_MetricsHistoryLiveFallbackJSONSnapshot(t *testing.T) {
|
|||
assertJSONSnapshot(t, got, want)
|
||||
}
|
||||
|
||||
func TestContract_MetricsHistoryProxmoxNodeCPUFallbackConvertsRatioToPercent(t *testing.T) {
|
||||
state := models.NewState()
|
||||
state.UpdateNodes([]models.Node{{
|
||||
ID: "pve1:node1",
|
||||
Name: "node1",
|
||||
Instance: "pve1",
|
||||
Status: "online",
|
||||
CPU: 0.37,
|
||||
}})
|
||||
|
||||
monitor := &monitoring.Monitor{}
|
||||
setUnexportedField(t, monitor, "state", state)
|
||||
setUnexportedField(t, monitor, "metricsHistory", monitoring.NewMetricsHistory(10, time.Hour))
|
||||
|
||||
tempDir := t.TempDir()
|
||||
mtp := config.NewMultiTenantPersistence(tempDir)
|
||||
if _, err := mtp.GetPersistence("default"); err != nil {
|
||||
t.Fatalf("failed to init persistence: %v", err)
|
||||
}
|
||||
|
||||
router := &Router{
|
||||
monitor: monitor,
|
||||
licenseHandlers: NewLicenseHandlers(mtp, false),
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
"/api/metrics-store/history?resourceType=node&resourceId=pve1:node1&metric=cpu&range=24h",
|
||||
nil,
|
||||
)
|
||||
rec := httptest.NewRecorder()
|
||||
router.handleMetricsHistory(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp metricsHistoryResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal metrics history response: %v", err)
|
||||
}
|
||||
if resp.Source != "live" {
|
||||
t.Fatalf("source = %q, want live", resp.Source)
|
||||
}
|
||||
if len(resp.Points) != 1 {
|
||||
t.Fatalf("points = %d, want 1", len(resp.Points))
|
||||
}
|
||||
if math.Abs(resp.Points[0].Value-37) > 0.001 {
|
||||
t.Fatalf("cpu fallback value = %f, want 37", resp.Points[0].Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContract_MetricsHistoryDockerContainerCPUFallbackUsesCapacityPercent(t *testing.T) {
|
||||
state := models.NewState()
|
||||
state.UpsertDockerHost(models.DockerHost{
|
||||
|
|
|
|||
|
|
@ -7540,6 +7540,10 @@ func clampWorkloadPercent(value float64) float64 {
|
|||
return value
|
||||
}
|
||||
|
||||
func proxmoxModelCPURatioPercent(value float64) float64 {
|
||||
return clampWorkloadPercent(value * 100)
|
||||
}
|
||||
|
||||
func clampNonNegativeWorkloadValue(value float64) float64 {
|
||||
if value != value {
|
||||
return 0
|
||||
|
|
@ -9010,7 +9014,7 @@ func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request)
|
|||
if vm == nil {
|
||||
return points
|
||||
}
|
||||
points["cpu"] = monitoring.MetricPoint{Timestamp: now, Value: vm.CPU}
|
||||
points["cpu"] = monitoring.MetricPoint{Timestamp: now, Value: proxmoxModelCPURatioPercent(vm.CPU)}
|
||||
points["memory"] = monitoring.MetricPoint{Timestamp: now, Value: vm.Memory.Usage}
|
||||
if vm.Disk.Usage >= 0 {
|
||||
points["disk"] = monitoring.MetricPoint{Timestamp: now, Value: vm.Disk.Usage}
|
||||
|
|
@ -9024,7 +9028,7 @@ func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request)
|
|||
if ct == nil {
|
||||
return points
|
||||
}
|
||||
points["cpu"] = monitoring.MetricPoint{Timestamp: now, Value: ct.CPU}
|
||||
points["cpu"] = monitoring.MetricPoint{Timestamp: now, Value: proxmoxModelCPURatioPercent(ct.CPU)}
|
||||
points["memory"] = monitoring.MetricPoint{Timestamp: now, Value: ct.Memory.Usage}
|
||||
if ct.Disk.Usage >= 0 {
|
||||
points["disk"] = monitoring.MetricPoint{Timestamp: now, Value: ct.Disk.Usage}
|
||||
|
|
@ -9038,7 +9042,7 @@ func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request)
|
|||
if node == nil {
|
||||
return points
|
||||
}
|
||||
points["cpu"] = monitoring.MetricPoint{Timestamp: now, Value: node.CPU}
|
||||
points["cpu"] = monitoring.MetricPoint{Timestamp: now, Value: proxmoxModelCPURatioPercent(node.CPU)}
|
||||
points["memory"] = monitoring.MetricPoint{Timestamp: now, Value: node.Memory.Usage}
|
||||
points["disk"] = monitoring.MetricPoint{Timestamp: now, Value: node.Disk.Usage}
|
||||
if temperature := primaryNodeTemperatureCelsius(node.Temperature); temperature != nil {
|
||||
|
|
@ -9093,7 +9097,7 @@ func (r *Router) handleMetricsHistory(w http.ResponseWriter, req *http.Request)
|
|||
if node == nil {
|
||||
return points
|
||||
}
|
||||
points["cpu"] = monitoring.MetricPoint{Timestamp: now, Value: node.CPU}
|
||||
points["cpu"] = monitoring.MetricPoint{Timestamp: now, Value: proxmoxModelCPURatioPercent(node.CPU)}
|
||||
points["memory"] = monitoring.MetricPoint{Timestamp: now, Value: node.Memory.Usage}
|
||||
points["disk"] = monitoring.MetricPoint{Timestamp: now, Value: node.Disk.Usage}
|
||||
if temperature := primaryNodeTemperatureCelsius(node.Temperature); temperature != nil {
|
||||
|
|
|
|||
|
|
@ -2917,7 +2917,7 @@ class SubsystemLookupTest(unittest.TestCase):
|
|||
{
|
||||
"heading": "## Shared Boundaries",
|
||||
"path": "internal/api/access_control_handlers.go",
|
||||
"line": 1168,
|
||||
"line": 1172,
|
||||
"heading_line": 137,
|
||||
}
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue