slice 9: GET /host/metrics + CPU/chassis-temp collector (v0.14.0)
Add a host-wide, token-authed GET /host/metrics local-API endpoint that re-serves the slice-4 collector's host + per-storage view to the customer (the de-privileged controller can't read the host itself). Add the one new collector — CPU/chassis temperature via sysfs hwmon/thermal-zones, graceful- null — to the shared HostMetrics struct, so the hub report carries cpu_temp_c too. Cross-repo host-report golden updated byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+35
-1
@@ -57,6 +57,7 @@ type Collector struct {
|
||||
backups BackupReporter
|
||||
restoreTests RestoreTestReporter
|
||||
pbs PBSReporter
|
||||
temp TempReader // slice 9: host CPU/chassis temp (nil-safe → nil temp)
|
||||
hostID string
|
||||
agentVersion string
|
||||
logger *slog.Logger
|
||||
@@ -76,6 +77,7 @@ func NewCollector(px proxmoxReader, cf CloudflaredProber, storage StorageObserve
|
||||
backups: backups,
|
||||
restoreTests: restoreTests,
|
||||
pbs: pbs,
|
||||
temp: SysfsTempReader{}, // slice 9: real sysfs reader by default; tests inject a fake
|
||||
hostID: hostID,
|
||||
agentVersion: agentVersion,
|
||||
logger: logger,
|
||||
@@ -83,6 +85,13 @@ func NewCollector(px proxmoxReader, cf CloudflaredProber, storage StorageObserve
|
||||
}
|
||||
}
|
||||
|
||||
// SetTempReader overrides the host-temp source (tests inject a fake; a nil reader disables temp).
|
||||
// Returns the collector for chaining.
|
||||
func (c *Collector) SetTempReader(t TempReader) *Collector {
|
||||
c.temp = t
|
||||
return c
|
||||
}
|
||||
|
||||
// Collect builds the report. Best-effort liveness: a failed NodeStatus is a hard
|
||||
// error (no useful report — the cycle skips the POST); a failed per-guest
|
||||
// GuestConfig degrades that guest to status="unknown" without spec but still sends;
|
||||
@@ -93,11 +102,13 @@ func (c *Collector) Collect(ctx context.Context) (*HostReport, error) {
|
||||
return nil, fmt.Errorf("hub: NodeStatus failed (no useful report): %w", err)
|
||||
}
|
||||
|
||||
host := hostMetrics(c.px.Node(), ns)
|
||||
host.CPUTempC = c.cpuTempC(ctx) // slice 9: operator freebie — temp now rides the hub report too
|
||||
report := &HostReport{
|
||||
HostID: c.hostID,
|
||||
ReportedAt: c.now().Format(time.RFC3339),
|
||||
AgentVersion: c.agentVersion,
|
||||
Host: hostMetrics(c.px.Node(), ns),
|
||||
Host: host,
|
||||
Guests: c.collectGuests(ctx),
|
||||
// storage_targets populated this slice (slice 5) via the observer; the rest stay
|
||||
// defined-but-empty (slice 6). Non-nil so they marshal as [].
|
||||
@@ -112,6 +123,29 @@ func (c *Collector) Collect(ctx context.Context) (*HostReport, error) {
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// HostMetricsNow does a FRESH NodeStatus + CPU-temp read and returns just the host block (no
|
||||
// guests/storage). It is the source for the local API's GET /host/metrics (slice 9) — current
|
||||
// cpu%/temp, not the 15-min hub-report snapshot. Storage targets come from the observer
|
||||
// separately. A NodeStatus failure is a hard error (no useful host view); a missing temp sensor
|
||||
// degrades to nil (never an error).
|
||||
func (c *Collector) HostMetricsNow(ctx context.Context) (HostMetrics, error) {
|
||||
ns, err := c.px.NodeStatus(ctx)
|
||||
if err != nil {
|
||||
return HostMetrics{}, fmt.Errorf("hub: NodeStatus failed: %w", err)
|
||||
}
|
||||
h := hostMetrics(c.px.Node(), ns)
|
||||
h.CPUTempC = c.cpuTempC(ctx)
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// cpuTempC reads the host CPU/chassis temp via the TempReader seam (nil-safe → nil).
|
||||
func (c *Collector) cpuTempC(ctx context.Context) *int {
|
||||
if c.temp == nil {
|
||||
return nil
|
||||
}
|
||||
return c.temp.CPUTempC(ctx)
|
||||
}
|
||||
|
||||
func hostMetrics(node string, ns proxmox.NodeStatus) HostMetrics {
|
||||
h := HostMetrics{
|
||||
Node: node,
|
||||
|
||||
Reference in New Issue
Block a user