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:
+102
-3
@@ -63,10 +63,109 @@ type Cloudflared struct {
|
||||
}
|
||||
|
||||
// The following element types are declared now so the empty collections above are
|
||||
// typed and slices 5/6 only fill them. No wire fields are committed yet.
|
||||
// typed and slices 5/6 only fill them.
|
||||
|
||||
type StorageTarget struct{} // slice 5: storage manifest target fields TBD
|
||||
type Backup struct{} // slice 6: per-target backup status fields TBD
|
||||
// StorageTarget is one observed host storage target (doc 03 §7). It is the REPORTED
|
||||
// shape — what the agent observes and tells the hub. The hub holds the AUTHORITATIVE
|
||||
// manifest (desired class/role/policy/creds, slice 10); the agent reports what it sees
|
||||
// and reconciles toward the hub's manifest. So `class_hint` is a rotational HINT, never
|
||||
// authoritative class, and `role` is set only when derivable from an existing Proxmox
|
||||
// storage definition (else empty — the hub owns it).
|
||||
//
|
||||
// This is a cross-repo contract DUPLICATED in felhom.eu/hub (no shared module yet);
|
||||
// testdata/host-report.golden.json must stay byte-identical with the hub's copy and the
|
||||
// bidirectional key-set test (contract_test.go) guards drift.
|
||||
type StorageTarget struct {
|
||||
Name string `json:"name"` // the Proxmox storage id (its name)
|
||||
Type string `json:"type"` // local-dir | lvmthin | usb | nfs | cifs | pbs | local
|
||||
DurableID string `json:"durable_id"` // fs-UUID (usb/local-dir) | server:export (nfs/cifs) | repo+fingerprint (pbs)
|
||||
// State is the observed lifecycle state. attached (present & usable) | disconnected
|
||||
// (a KNOWN target whose backing device/mount/reachability dropped) | decommissioned
|
||||
// (hub-manifest state — built but not served until slice 10).
|
||||
State string `json:"state"`
|
||||
Reachable bool `json:"reachable"` // backing device present + mounted (local) / reachable (network)
|
||||
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
UsedBytes int64 `json:"used_bytes"`
|
||||
AvailBytes int64 `json:"avail_bytes"`
|
||||
UsedFraction float64 `json:"used_fraction"`
|
||||
Content string `json:"content"` // Proxmox content list, e.g. "rootdir,images" / "backup,vztmpl"
|
||||
|
||||
MountPath string `json:"mount_path"` // host mountpoint (dir/usb); "" for network/lvm
|
||||
BackingDevice string `json:"backing_device"` // resolved block device (e.g. /dev/sdb1); "" for network
|
||||
// ClassHint is a fast|slow HINT derived from the backing disk's rotational flag — a
|
||||
// hint only; the authoritative class is hub-owned (locked decision). "" when not
|
||||
// derivable (network targets have no local rotational flag).
|
||||
ClassHint string `json:"class_hint"`
|
||||
// Role is primary|vzdump-target|pbs-offsite|bulk-data when derivable from the existing
|
||||
// storage definition; else "" (the manifest role is hub-owned, slice 10).
|
||||
Role string `json:"role"`
|
||||
|
||||
// ThinPool carries the lvmthin DATA fill prominently — a full thin-pool corrupts every
|
||||
// guest on it (the storage analog of single-node OOM). Present ONLY for lvmthin targets;
|
||||
// metadata fill is null until Phase B's privileged `lvs` read.
|
||||
ThinPool *ThinPoolFill `json:"thin_pool,omitempty"`
|
||||
|
||||
// Smart is the disk-health summary, populated in Phase B via smartctl (sudoers). In
|
||||
// Phase A it is {health:"UNKNOWN", <counters null>} — the keys are committed now so the
|
||||
// contract is stable across both phases.
|
||||
Smart SmartSummary `json:"smart"`
|
||||
}
|
||||
|
||||
// ThinPoolFill is the lvmthin pool fill (doc 03 §7). DataUsedFraction is the live data
|
||||
// fill (used/total of the lvmthin store); MetadataUsedFraction needs the privileged
|
||||
// `lvs` read (Phase B) and is null until then.
|
||||
type ThinPoolFill struct {
|
||||
DataUsedFraction float64 `json:"data_used_fraction"`
|
||||
MetadataUsedFraction *float64 `json:"metadata_used_fraction"`
|
||||
}
|
||||
|
||||
// SmartSummary is a read-only disk-health summary. Health is PASSED|FAILING|UNKNOWN.
|
||||
// Counters are pointers so "unknown / not-applicable for this device type" (e.g. a USB
|
||||
// bridge that exposes no SMART, or NVMe counters on a SATA disk) is null, distinct from a
|
||||
// real zero. The SATA set (reallocated/pending/offline-uncorrectable) and the NVMe set
|
||||
// (critical_warning/media_errors/percentage_used) are both carried; a device populates
|
||||
// only its own set. Filled in Phase B.
|
||||
type SmartSummary struct {
|
||||
Health string `json:"health"`
|
||||
|
||||
TemperatureC *int `json:"temperature_c"`
|
||||
PowerOnHours *int `json:"power_on_hours"`
|
||||
|
||||
// SATA attributes.
|
||||
ReallocatedSectors *int `json:"reallocated_sectors"`
|
||||
PendingSectors *int `json:"pending_sectors"`
|
||||
OfflineUncorrectable *int `json:"offline_uncorrectable"`
|
||||
|
||||
// NVMe attributes.
|
||||
CriticalWarning *int `json:"critical_warning"`
|
||||
MediaErrors *int `json:"media_errors"`
|
||||
PercentageUsed *int `json:"percentage_used"`
|
||||
}
|
||||
|
||||
// SMART health constants (the reported vocabulary).
|
||||
const (
|
||||
SmartPassed = "PASSED"
|
||||
SmartFailing = "FAILING"
|
||||
SmartUnknown = "UNKNOWN"
|
||||
)
|
||||
|
||||
// Storage target type + state constants (the reported vocabulary; doc 03 §7).
|
||||
const (
|
||||
StorageTypeLocalDir = "local-dir"
|
||||
StorageTypeLVMThin = "lvmthin"
|
||||
StorageTypeUSB = "usb"
|
||||
StorageTypeNFS = "nfs"
|
||||
StorageTypeCIFS = "cifs"
|
||||
StorageTypePBS = "pbs"
|
||||
StorageTypeLocal = "local" // builtin dir storage (PVE "local")
|
||||
|
||||
StorageStateAttached = "attached"
|
||||
StorageStateDisconnected = "disconnected"
|
||||
StorageStateDecommissioned = "decommissioned"
|
||||
)
|
||||
|
||||
type Backup struct{} // slice 6: per-target backup status fields TBD
|
||||
type RestoreTest struct{} // slice 6: self-restore-test result fields TBD
|
||||
type PBSSnapshot struct{} // slice 6: PBS snapshot inventory fields TBD
|
||||
type AuditEntry struct{} // audit-log tail entry fields TBD
|
||||
|
||||
Reference in New Issue
Block a user