v0.5.0-rc1: slice 5 Phase A — storage observe/report + watchdog (read-only, live)
Fill the slice-3 storage_targets stub and add the fast-poll storage watchdog. Read-only this phase; the host-root surface (mounts/SMART/grow/destructive gate) is Phase B. Hub-owned desired manifest is slice 10, so reconcile against it is built-but-unfed. - internal/storage: StorageTarget wire contract, durable_id derivation per type, HostReader seam (procfs/sysfs, root-free), Observer (storage_targets from ListStorage/NodeStorage + host reads, lvmthin thin-pool fill), and the watchdog (third daemon goroutine; debounced out-of-band report on a known target's attach/disconnect transition). - proxmox.Storage: additive parse-only config fields (durable_id sources). - collector StorageObserver seam; Loop.SetTrigger out-of-band report; daemon runs the watchdog as a third goroutine; StorageConfig knobs. - cross-repo golden kept byte-identical with felhom.eu/hub; bidirectional key-set test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+34
-4
@@ -22,11 +22,21 @@ type proxmoxReader interface {
|
||||
GuestConfig(ctx context.Context, vmid int) (proxmox.GuestConfig, error)
|
||||
}
|
||||
|
||||
// StorageObserver is the seam the storage layer (internal/storage) plugs into to fill the
|
||||
// report's storage_targets. Defined here (consumer-side) so hub does NOT import storage —
|
||||
// storage imports hub for the wire type, and main.go wires the concrete observer in. Same
|
||||
// pattern as proxmoxReader / CloudflaredProber. A nil observer (slice-3 behaviour, or a
|
||||
// host with no storage layer) yields an empty []StorageTarget without error.
|
||||
type StorageObserver interface {
|
||||
Observe(ctx context.Context) ([]StorageTarget, error)
|
||||
}
|
||||
|
||||
// Collector builds a HostReport from read-only sources. All deps are behind narrow
|
||||
// interfaces for unit testing.
|
||||
type Collector struct {
|
||||
px proxmoxReader
|
||||
cf CloudflaredProber
|
||||
storage StorageObserver
|
||||
hostID string
|
||||
agentVersion string
|
||||
logger *slog.Logger
|
||||
@@ -34,14 +44,15 @@ type Collector struct {
|
||||
}
|
||||
|
||||
// NewCollector builds a collector. hostID echoes config.Hub.HostID; agentVersion is
|
||||
// the binary version.
|
||||
func NewCollector(px proxmoxReader, cf CloudflaredProber, hostID, agentVersion string, logger *slog.Logger) *Collector {
|
||||
// the binary version. storage may be nil (storage_targets emitted empty).
|
||||
func NewCollector(px proxmoxReader, cf CloudflaredProber, storage StorageObserver, hostID, agentVersion string, logger *slog.Logger) *Collector {
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
return &Collector{
|
||||
px: px,
|
||||
cf: cf,
|
||||
storage: storage,
|
||||
hostID: hostID,
|
||||
agentVersion: agentVersion,
|
||||
logger: logger,
|
||||
@@ -65,8 +76,9 @@ func (c *Collector) Collect(ctx context.Context) (*HostReport, error) {
|
||||
AgentVersion: c.agentVersion,
|
||||
Host: hostMetrics(c.px.Node(), ns),
|
||||
Guests: c.collectGuests(ctx),
|
||||
// Defined-but-empty this slice (slices 5/6). Non-nil so they marshal as [].
|
||||
StorageTargets: []StorageTarget{},
|
||||
// storage_targets populated this slice (slice 5) via the observer; the rest stay
|
||||
// defined-but-empty (slice 6). Non-nil so they marshal as [].
|
||||
StorageTargets: c.collectStorage(ctx),
|
||||
Backups: []Backup{},
|
||||
RestoreTests: []RestoreTest{},
|
||||
PBSSnapshots: []PBSSnapshot{},
|
||||
@@ -129,6 +141,24 @@ func (c *Collector) collectGuests(ctx context.Context) []Guest {
|
||||
return guests
|
||||
}
|
||||
|
||||
// collectStorage builds the storage_targets via the observer. A nil observer (no storage
|
||||
// layer wired) or an observe error degrades to an empty list — storage detail is
|
||||
// best-effort and must never sink the heartbeat (host liveness is the priority).
|
||||
func (c *Collector) collectStorage(ctx context.Context) []StorageTarget {
|
||||
if c.storage == nil {
|
||||
return []StorageTarget{}
|
||||
}
|
||||
targets, err := c.storage.Observe(ctx)
|
||||
if err != nil {
|
||||
c.logger.Warn("hub: storage observe failed; reporting no storage targets", "err", err)
|
||||
return []StorageTarget{}
|
||||
}
|
||||
if targets == nil {
|
||||
return []StorageTarget{}
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
func (c *Collector) cloudflaredStatus(ctx context.Context) string {
|
||||
if c.cf == nil {
|
||||
return "unknown"
|
||||
|
||||
Reference in New Issue
Block a user