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:
@@ -25,11 +25,12 @@ import (
|
||||
applog "gitea.dooplex.hu/admin/felhom-agent/internal/log"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/reconcile"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
|
||||
)
|
||||
|
||||
// version is the agent version. Overridable at build time with
|
||||
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
|
||||
var version = "0.4.0"
|
||||
var version = "0.5.0-rc1"
|
||||
|
||||
func main() {
|
||||
var (
|
||||
@@ -109,10 +110,34 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
|
||||
return 1
|
||||
}
|
||||
hcfg := cfg.Hub.WithDefaults()
|
||||
collector := hub.NewCollector(px, hub.SystemctlProber{}, cfg.Hub.HostID, version, logger)
|
||||
// Storage observer (slice 5): read-only, builds the report's storage_targets from
|
||||
// Proxmox + non-privileged host reads. Wired into the collector via the StorageObserver
|
||||
// seam (so hub does not import storage).
|
||||
hostReader := storage.NewProcHostReader()
|
||||
observer := storage.NewObserver(px, hostReader, logger)
|
||||
collector := hub.NewCollector(px, hub.SystemctlProber{}, observer, cfg.Hub.HostID, version, logger)
|
||||
loop := hub.NewLoop(collector, client, time.Duration(hcfg.PollSeconds)*time.Second, logger)
|
||||
interval := time.Duration(hcfg.PollSeconds) * time.Second
|
||||
|
||||
// Storage watchdog (slice 5): a third daemon goroutine fast-polling the known target
|
||||
// set for attached↔disconnected transitions and triggering an immediate, debounced
|
||||
// out-of-band report. With no removable/network storage it simply finds nothing to flag.
|
||||
storageTrigger := make(chan struct{}, 1)
|
||||
loop.SetTrigger(storageTrigger)
|
||||
watchdog := storage.NewWatchdog(storage.WatchdogOptions{
|
||||
Targets: storage.NewCachingKnownTargets(observer, cfg.Storage.KnownRefresh()),
|
||||
Liveness: storage.NewHostLiveness(hostReader, 0),
|
||||
Trigger: func() {
|
||||
select {
|
||||
case storageTrigger <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
},
|
||||
Interval: cfg.Storage.WatchdogInterval(),
|
||||
Debounce: cfg.Storage.WatchdogDebounce(),
|
||||
Logger: logger,
|
||||
})
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
logger.Info("felhom-agent daemon starting",
|
||||
@@ -167,14 +192,17 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
|
||||
// last died BEFORE issuing new mutations. With an empty journal this is a no-op.
|
||||
engine.Recover(ctx)
|
||||
|
||||
// Run reconcile and the hub loop concurrently; either returning ends the daemon.
|
||||
errc := make(chan error, 2)
|
||||
// Run reconcile, the hub loop, and the storage watchdog concurrently; any one
|
||||
// returning ends the daemon (then ctx cancellation tears the others down).
|
||||
errc := make(chan error, 3)
|
||||
go func() { errc <- engine.Run(ctx, interval) }()
|
||||
go func() { errc <- loop.Run(ctx) }()
|
||||
go func() { errc <- watchdog.Run(ctx) }()
|
||||
|
||||
err = <-errc
|
||||
stop() // tear down the sibling on the first exit
|
||||
<-errc // wait for it
|
||||
stop() // tear down the siblings on the first exit
|
||||
<-errc // wait for the second
|
||||
<-errc // wait for the third
|
||||
if err != nil && err != context.Canceled {
|
||||
logger.Error("daemon: exited with error", "err", err)
|
||||
return 1
|
||||
@@ -242,7 +270,8 @@ func runSelftestHub(ctx context.Context, cfg config.Config, logger *slog.Logger)
|
||||
fmt.Fprintln(os.Stderr, "selftest: hub client:", err)
|
||||
return 1
|
||||
}
|
||||
collector := hub.NewCollector(px, hub.SystemctlProber{}, cfg.Hub.HostID, version, logger)
|
||||
observer := storage.NewObserver(px, storage.NewProcHostReader(), logger)
|
||||
collector := hub.NewCollector(px, hub.SystemctlProber{}, observer, cfg.Hub.HostID, version, logger)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
Reference in New Issue
Block a user