Files
diskless-workstation/serving/fleet-manager/inventory.go
T
Audiobuzz 1c68c37824 fleet-manager: chrony tracking metrics relative to the NTP source
The agent only reported chrony as synced|unsynced — nothing about how well the
clock tracks. Parse `chronyc tracking` for the source-relative metrics.

- proto: HealthSnapshot.chrony_offset_seconds (16, signed system-time offset,
  fast=+), chrony_source (17, reference name/IP), chrony_stratum (18); regenerated.
- agent: replace chronyState() with chronyTracking()/parseChrony() — one exec,
  parses Leap status + Reference ID + Stratum + System time offset (+ test).
- manager: Health.{ChronyOffset,ChronySource,ChronyStratum}; snapshotToHealth maps
  them; influx writes chrony_offset_seconds/_stratum/_source and whitelists the
  offset + stratum as graphable fields.
- UI: chronySummary() + offsetMs() — the chrony row now shows
  "synced · 192.168.50.10 · s3 · +0.01ms" instead of just "synced".
2026-07-10 06:47:27 +00:00

120 lines
5.9 KiB
Go

// inventory.go — the fleet inventory record and the Store abstraction.
//
// The Host record is the join key the rest of the fleet-manager hangs off. It is
// BORN at enrolment approval (identity/binding/enrolled), then FILLED by the
// steady-state agent stream (version/health/usage) and by admin action (desired).
// Schema mirrors docs/fleet-management-design.md "Inventory record" and
// docs/tpm-enrolment-design.md "Inventory record born at enrolment".
//
// Keyed by the TPM cert SERIAL (lowercase hex) — identical to the value the NVMe
// hostnqn derives from and the value the agent's mTLS leaf presents, so the cert
// an agent shows IS its inventory key (no identity translation). See §5 identity
// discussion.
package main
import "time"
// Identity — minted at enrolment, immutable afterwards. CertSerial is the primary
// key; Hostnqn = HOSTNQN_BASE + CertSerial (as the broker computes it on approve).
type Identity struct {
CertSerial string `json:"cert_serial"` // lowercase hex — PRIMARY KEY
Hostnqn string `json:"hostnqn,omitempty"` // nqn...:host-<serial>
MAC string `json:"mac,omitempty"` // advisory (DeviceContext at enrol)
EKHash string `json:"ek_hash,omitempty"` // TPM EK hash (advisory)
StarshipName string `json:"starship_name,omitempty"` // dnsmasq fleet-namer join
}
// Binding — the physical↔crypto tie the human records during the OOB approval.
type Binding struct {
AssetTag string `json:"asset_tag,omitempty"`
SwitchPort string `json:"switch_port,omitempty"`
Location string `json:"location,omitempty"`
}
// Enrolled — provenance of the approval that birthed this record.
type Enrolled struct {
Approver string `json:"approver,omitempty"` // admin CN from the approve mTLS
At time.Time `json:"at,omitzero"` // approval timestamp
Fingerprint string `json:"fingerprint,omitempty"` // the human-verified anchor
}
// Version — what the host is actually running (agent-reported).
type Version struct {
ImageVersion string `json:"image_version,omitempty"`
RootfsDigest string `json:"rootfs_digest,omitempty"`
Kernel string `json:"kernel,omitempty"`
}
// Health — the LATEST snapshot only. The full time-series lives in InfluxDB
// (written by the manager as agent reports arrive); this cached last value is
// what the fleet-overview list renders without a per-host Flux query.
type Health struct {
LoadAvg float64 `json:"load_avg,omitempty"`
CPUPct float64 `json:"cpu_pct,omitempty"`
TempC float64 `json:"temp_c,omitempty"`
MemUsedPct float64 `json:"mem_used_pct,omitempty"`
TmpfsUsedPct float64 `json:"tmpfs_used_pct,omitempty"` // overlay RAM pressure (ties to #17)
NVMeRoot string `json:"nvme_root,omitempty"` // ok|degraded|down
Auth string `json:"auth,omitempty"` // 802.1x/macsec: up|down
SELinux string `json:"selinux,omitempty"` // enforcing|permissive
Chrony string `json:"chrony,omitempty"` // synced|unsynced
ChronyOffset float64 `json:"chrony_offset_seconds,omitempty"` // system time offset from NTP (signed, fast=+)
ChronySource string `json:"chrony_source,omitempty"` // NTP reference source name/IP
ChronyStratum int32 `json:"chrony_stratum,omitempty"` // hops from a reference clock
KioskActive bool `json:"kiosk_active"`
UptimeSeconds int64 `json:"uptime_seconds,omitempty"` // host uptime at last report; boot_time = last_seen - this (reboot-pending)
}
// Usage — kiosk-session facts (agent-reported).
type Usage struct {
KioskURL string `json:"kiosk_url,omitempty"`
SessionUptime string `json:"session_uptime,omitempty"`
ChromiumCrashes int `json:"chromium_crashes,omitempty"`
Display string `json:"display,omitempty"`
}
// Desired — SERVER-AUTHORITATIVE target state. Set via the admin lifecycle API,
// persisted here, and pushed to the host over its live agent stream (or delivered
// on its next reconnect if offline). Inventory is the source of truth; the stream
// is only the delivery optimisation.
type Desired struct {
TargetVersion string `json:"target_version,omitempty"`
KioskURL string `json:"kiosk_url,omitempty"`
RebootAt *time.Time `json:"reboot_at,omitempty"`
Drain bool `json:"drain,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"` // admin CN
UpdatedAt time.Time `json:"updated_at,omitzero"`
}
// Host is the full inventory record.
type Host struct {
Identity Identity `json:"identity"`
Binding Binding `json:"binding"`
Enrolled Enrolled `json:"enrolled"`
Version Version `json:"version"`
Health Health `json:"health"`
Usage Usage `json:"usage"`
Desired Desired `json:"desired"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"` // last agent report / stream activity
Online bool `json:"online"` // agent stream currently connected
}
// Store is the inventory persistence abstraction. The v1 implementation is a
// JSON-file store (jsonStore); the interface keeps a DB swap a drop-in later.
// All methods are safe for concurrent use.
type Store interface {
// List returns a snapshot of every host (copies — safe to mutate).
List() []*Host
// Get returns a copy of one host by cert serial.
Get(serial string) (*Host, bool)
// Upsert creates or replaces a host record (used by enrolment approval).
Upsert(h *Host) error
// SetDesired replaces the desired-state block and returns the updated host.
SetDesired(serial string, d Desired) (*Host, bool, error)
// ReportState applies an agent snapshot (version/health/usage + last-seen).
ReportState(serial string, v Version, h Health, u Usage, at time.Time) (*Host, bool, error)
// SetOnline flips the live-stream presence flag (and stamps last-seen on connect).
SetOnline(serial string, online bool, at time.Time)
}