fleet-manager: host uptime metric + reboot-pending status

The manager lets an admin schedule a reboot (desired.reboot_at) but never showed
whether it had happened. Add host uptime to the health snapshot and derive a
reboot status from it.

- proto: HealthSnapshot.uptime_seconds (15); regenerated pb.go (manager + agent copy).
- agent: probe /proc/uptime -> uptime_seconds (with parser + test).
- manager: Health.UptimeSeconds cached in inventory; snapshotToHealth maps it;
  influx writes + whitelists uptime_seconds as a queryable field.
- UI: fmt.js dur() + rebootStatus() — boot_time = last_seen - uptime (manager clock
  frame, no host/manager sync needed), compared to reboot_at. Monitor host detail
  shows an "uptime" row and a "reboot" row: pending in X / overdue by X / rebooted.

Observability-only — works before the reboot ACTUATOR lands (deferred). Once a host
boots after the scheduled time (BMC/manual now, agent later), the badge flips to
"rebooted" automatically.
This commit is contained in:
Audiobuzz
2026-07-10 06:41:09 +00:00
parent e6ae8c4b23
commit df7dad81b7
11 changed files with 128 additions and 29 deletions
+24
View File
@@ -98,10 +98,34 @@ func (p *prober) snapshot(ctx context.Context) *fleetpb.HealthSnapshot {
Chrony: chronyState(),
KioskActive: unitActive("kiosk.service"),
SessionUptime: f.Kiosk.Uptime,
UptimeSeconds: uptimeSeconds(),
}
return s
}
// uptimeSeconds returns host uptime (seconds since boot) from /proc/uptime. The
// manager derives boot_time = receive-time - this to tell whether a scheduled
// reboot has been actioned yet.
func uptimeSeconds() int64 {
b, err := os.ReadFile("/proc/uptime")
if err != nil {
return 0
}
return parseUptime(b)
}
func parseUptime(b []byte) int64 {
fields := strings.Fields(string(b))
if len(fields) == 0 {
return 0
}
f, _ := strconv.ParseFloat(fields[0], 64)
if f < 0 {
return 0
}
return int64(f)
}
// ---- individual probes (all best-effort) ----------------------------------
func loadAvg() float64 {