hub v0.7.2: ingest agent storage_targets (slice 5 Phase A)
Accept + persist the now-populated host-report storage_targets. Minimal — the authoritative storage manifest is hub-owned (slice 10); this mirrors what the agent observes. - hostReportPayload.StorageTargets: full mirror of the agent's hub.StorageTarget wire contract; persisted verbatim in report_json (no schema change); count + WARN on disconnected targets. - shared host-report golden updated with two populated targets; byte-identical with felhom-agent's copy. - TestHostStorageTarget_GoldenContract: hub half of the bidirectional key-set test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -234,9 +234,15 @@ const defaultHostPollSeconds = 900
|
||||
const maxHostReportBytes = 4 << 20 // 4 MiB
|
||||
|
||||
// hostReportPayload is the subset of the agent host-report (slice-3 contract,
|
||||
// §3 / agent spec §4) the hub needs for denorm + guest reality. Unknown fields
|
||||
// (storage_targets/backups/restore_tests/pbs_snapshots/audit_tail) are ignored,
|
||||
// so an empty or absent collection is accepted without error.
|
||||
// §3 / agent spec §4) the hub needs for denorm + guest reality. The remaining fields
|
||||
// (backups/restore_tests/pbs_snapshots/audit_tail) are ignored, so an empty or absent
|
||||
// collection is accepted without error.
|
||||
//
|
||||
// storage_targets (slice 5) is now parsed: the agent populates it, and the hub accepts
|
||||
// + persists it. Persistence is the full report_json row (which carries the targets
|
||||
// verbatim) plus the denorm counts below — the RICH manifest schema (desired class/role/
|
||||
// policy/creds) is hub-owned and lands in slice 10; this slice only mirrors what the agent
|
||||
// observes.
|
||||
type hostReportPayload struct {
|
||||
HostID string `json:"host_id"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
@@ -251,11 +257,49 @@ type hostReportPayload struct {
|
||||
Status string `json:"status"`
|
||||
ControllerVersion string `json:"controller_version"`
|
||||
} `json:"guests"`
|
||||
Cloudflared struct {
|
||||
StorageTargets []hostStorageTarget `json:"storage_targets"`
|
||||
Cloudflared struct {
|
||||
Status string `json:"status"`
|
||||
} `json:"cloudflared"`
|
||||
}
|
||||
|
||||
// hostStorageTarget mirrors the agent's hub.StorageTarget wire contract field-for-field.
|
||||
// It is a DUPLICATED contract (no shared types module yet); testdata/host-report.golden.json
|
||||
// must stay byte-identical with felhom-agent's copy and the key-set test guards drift.
|
||||
// The hub does not act on these yet beyond persisting + counting them (slice 10 adds the
|
||||
// authoritative manifest), but mirroring the full shape keeps the cross-repo contract honest.
|
||||
type hostStorageTarget struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
DurableID string `json:"durable_id"`
|
||||
State string `json:"state"`
|
||||
Reachable bool `json:"reachable"`
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
UsedBytes int64 `json:"used_bytes"`
|
||||
AvailBytes int64 `json:"avail_bytes"`
|
||||
UsedFraction float64 `json:"used_fraction"`
|
||||
Content string `json:"content"`
|
||||
MountPath string `json:"mount_path"`
|
||||
BackingDevice string `json:"backing_device"`
|
||||
ClassHint string `json:"class_hint"`
|
||||
Role string `json:"role"`
|
||||
ThinPool *struct {
|
||||
DataUsedFraction float64 `json:"data_used_fraction"`
|
||||
MetadataUsedFraction *float64 `json:"metadata_used_fraction"`
|
||||
} `json:"thin_pool,omitempty"`
|
||||
Smart struct {
|
||||
Health string `json:"health"`
|
||||
TemperatureC *int `json:"temperature_c"`
|
||||
PowerOnHours *int `json:"power_on_hours"`
|
||||
ReallocatedSectors *int `json:"reallocated_sectors"`
|
||||
PendingSectors *int `json:"pending_sectors"`
|
||||
OfflineUncorrectable *int `json:"offline_uncorrectable"`
|
||||
CriticalWarning *int `json:"critical_warning"`
|
||||
MediaErrors *int `json:"media_errors"`
|
||||
PercentageUsed *int `json:"percentage_used"`
|
||||
} `json:"smart"`
|
||||
}
|
||||
|
||||
// handleHostReport ingests the agent's host-report (the heartbeat) and returns the
|
||||
// control envelope (agent spec §5).
|
||||
func (h *Handler) handleHostReport(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -340,7 +384,22 @@ func (h *Handler) handleHostReport(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Printf("[INFO] host-report from %s (%d guests, %d bytes)", hostID, len(rep.Guests), len(body))
|
||||
// storage_targets (slice 5): persisted as part of report_json above. Count + surface
|
||||
// disconnected ones in the log (the slice-10 manifest will reconcile them; for now the
|
||||
// signal is the visibility — a disconnected target is the storage analog of host-down).
|
||||
disconnected := 0
|
||||
for _, st := range rep.StorageTargets {
|
||||
if st.State == "disconnected" {
|
||||
disconnected++
|
||||
}
|
||||
}
|
||||
if disconnected > 0 {
|
||||
h.logger.Printf("[WARN] host %s reports %d disconnected storage target(s) of %d",
|
||||
hostID, disconnected, len(rep.StorageTargets))
|
||||
}
|
||||
|
||||
h.logger.Printf("[INFO] host-report from %s (%d guests, %d storage targets, %d bytes)",
|
||||
hostID, len(rep.Guests), len(rep.StorageTargets), len(body))
|
||||
|
||||
blocked := false
|
||||
if cc, err := h.store.GetCustomerConfig(custID); err == nil && cc != nil && cc.Status == "blocked" {
|
||||
|
||||
Reference in New Issue
Block a user