Files
diskless-workstation/serving/fleet-manager/inventory.go
T
Audiobuzz 80cbb833cc fleet-manager: checkpoint 2 — gRPC agent plane (bidi mTLS stream)
The steady-state host<->manager transport, resolving the design's OPEN FORK to
bespoke gRPC (not OTLP+Wazuh). One long-lived, host-dialed, mTLS bidi stream:
health/metrics up, desired-state/commands down — no polling, real-time control,
still no inbound host port.

- proto/fleet_agent.proto: FleetAgent.Session(stream AgentUp) -> stream ManagerDown.
  AgentUp = Hello | HealthSnapshot | CommandResult; ManagerDown = DesiredState |
  Command(reboot/drain/resync). int64 unix times (no well-known-type dep).
  Generated stubs committed (CI needs no protoc).
- agent.go: gRPC server with mTLS verified by the FLEET CA (the host's reused TPM
  identity cert — distinct from the admin plane's admin CA). Identity is the peer
  cert SERIAL, taken from the verified cert = inventory key, never self-asserted;
  an unenrolled serial is refused (enrolment gates the stream). Per-conn send
  channel decouples admin pushes from stream.Send. Stream registry (serial->conn)
  with reconnect eviction. On connect + on change, desired-state is pushed live;
  inventory stays authoritative so offline hosts reconcile on reconnect. Metrics
  go through a MetricsSink seam (no-op now; InfluxDB in ckpt 4).
- main.go: agent plane starts only when its cert/key/CA are set; admin can run
  standalone. Lifecycle PUT now live-pushes and reports pushed_live.

Full mTLS integration test (in-process fleet CA + host client cert): initial
desired-state on connect, Hello+snapshot -> inventory + online presence, live
desired-state push, and unenrolled-serial rejection. go build/test/vet/gofmt clean.

Deps (server-side only): google.golang.org/grpc + protobuf.
2026-07-04 03:40:55 +00:00

116 lines
5.4 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
KioskActive bool `json:"kiosk_active"`
}
// 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)
}