agent v0.27.0: slice 10 P3 — self-heal watchdog reconcile + 4-state intent model
IntentStore (durable-id-keyed: new/enrolled/ejected/decommissioned, OnAbsent replug rule). Watchdog re-mounts only enrolled drives (out-of-band unmount heals; ejected/decommissioned/new left alone) + exp-backoff flapping guard (alert@4, cap@8). guest-attach records enrolled; eject records ejected. Non-hollow tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,7 +43,7 @@ import (
|
||||
|
||||
// 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.26.0"
|
||||
var version = "0.27.0"
|
||||
|
||||
func main() {
|
||||
var (
|
||||
@@ -329,10 +329,27 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
|
||||
storageTrigger := make(chan struct{}, 1)
|
||||
loop.SetTrigger(storageTrigger)
|
||||
remounter := &gateRemounter{gate: gate, ops: hostOps, hostID: cfg.Hub.HostID, logger: logger}
|
||||
// Drive intent store (slice 10 P3 self-heal): persisted, durable-id-keyed enroll/eject/decommission
|
||||
// state. Gates the watchdog's self-heal re-mount to ENROLLED drives, and the local API records
|
||||
// enroll/eject into it. Open failure → ungated legacy remount (logged) so the daemon still runs.
|
||||
var intentReader storage.IntentReader
|
||||
intentStore, ierr := storage.OpenIntentStore(filepath.Join(cfg.LocalAPI.StateDir, "drive-intents.json"))
|
||||
if ierr != nil {
|
||||
logger.Warn("storage: intent store unavailable — self-heal runs UNGATED (legacy remount-any)", "err", ierr)
|
||||
intentStore = nil
|
||||
} else {
|
||||
intentReader = intentStore
|
||||
}
|
||||
watchdog := storage.NewWatchdog(storage.WatchdogOptions{
|
||||
Targets: storage.NewCachingKnownTargets(observer, cfg.Storage.KnownRefresh()),
|
||||
Liveness: storage.NewHostLiveness(hostReader, 0),
|
||||
Remounter: remounter,
|
||||
Intent: intentReader, // P3: only re-mount enrolled drives
|
||||
OnAbsent: func(durableID string) { // P3: clear `ejected` on physical absence (replug rule)
|
||||
if intentStore != nil {
|
||||
_ = intentStore.OnAbsent(durableID)
|
||||
}
|
||||
},
|
||||
Trigger: func() {
|
||||
select {
|
||||
case storageTrigger <- struct{}{}:
|
||||
@@ -381,7 +398,11 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
|
||||
// host still reports/reconciles). The leaf is generated+persisted once so its pin is stable.
|
||||
localServers := 0
|
||||
var localTokens *localapi.TokenStore
|
||||
localSrv := buildLocalAPIServer(cfg, px, backupStore, observer, hostOps, gate, collector, logger, &localTokens)
|
||||
var intentRec localapi.IntentRecorder
|
||||
if intentStore != nil { // avoid a typed-nil interface (would defeat the nil check)
|
||||
intentRec = intentStore
|
||||
}
|
||||
localSrv := buildLocalAPIServer(cfg, px, backupStore, observer, hostOps, gate, collector, intentRec, logger, &localTokens)
|
||||
if localTokens != nil {
|
||||
defer localTokens.Close()
|
||||
}
|
||||
@@ -532,7 +553,7 @@ func buildRestoreTestScheduler(cfg config.Config, px *proxmox.Client, engine *re
|
||||
// leaf (stable fingerprint). Any failure DISABLES the server (returns nil) WITHOUT crashing the
|
||||
// daemon — the host still reports/reconciles; only the controller channel is unavailable until
|
||||
// fixed. The opened token store is returned via outTokens so the caller can Close it.
|
||||
func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.Store, observer *storage.Observer, hostOps storage.HostOps, gate *reconcile.Gate, collector *hub.Collector, logger *slog.Logger, outTokens **localapi.TokenStore) *localapi.Server {
|
||||
func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.Store, observer *storage.Observer, hostOps storage.HostOps, gate *reconcile.Gate, collector *hub.Collector, intent localapi.IntentRecorder, logger *slog.Logger, outTokens **localapi.TokenStore) *localapi.Server {
|
||||
if !cfg.LocalAPI.Enabled() {
|
||||
return nil
|
||||
}
|
||||
@@ -575,6 +596,7 @@ func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.St
|
||||
DiskGate: storageGateAdapter{gate: gate, hostID: cfg.Hub.HostID},
|
||||
Guests2: px,
|
||||
GuestAttach: guestBinder, // slice 10 P2: bind enrolled data drives into the guest
|
||||
Intent: intent, // slice 10 P3: record enroll/eject intent for self-heal
|
||||
|
||||
// Host metrics (slice 9): the shared collector serves GET /host/metrics — a fresh host +
|
||||
// per-storage view to the customer's monitoring page (reuses the slice-4 collector).
|
||||
|
||||
Reference in New Issue
Block a user