v0.55.0: raw-device discovery + registry-sourced drive tracking (Impl-2a)

GET /disks/candidates enumerates host disks the Impl-1 unclaimed filter proves
free (init/attach split). RegistryKnownTargets sources the watchdog's known-drive
set from the intent registry + Felhom .mount units (not Observe/PVE storages) —
decouples drive health from PVE storage (closes the registry-only false-detach
class); Observe kept for real PVE storages + a deduped /disks union. Idempotent
existing-drive migration at start. Tests + red-proof (Observe misses a
registry-only drive; registry provider tracks it). go build/vet/test clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 17:33:49 +02:00
parent 066e3bf153
commit 91f6a26490
10 changed files with 488 additions and 5 deletions
+20 -4
View File
@@ -45,7 +45,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.54.0"
var version = "0.55.0"
// runGuestHook is the PVE pre-start hook body (`felhom-agent guest-hook <vmid> <phase>`). On the
// pre-start phase it creates placeholder dirs for any absent bind-mount source so the guest always boots
@@ -429,8 +429,23 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
logger.Warn("storage: format-job store unavailable — format restart-recovery disabled (F20-BUG3)", "err", fjErr)
formatJobStore = nil
}
// Impl-2a: source the watchdog's known-DRIVE set from the intent registry + Felhom .mount units
// (NOT PVE storages via Observe) — a registry-only drive (no PVE dir-storage) is now health-tracked.
// Real PVE storages (local/local-lvm/pbs) stay Observe-sourced for /disks + reports (observer below).
driveUnitDir := cfg.Privileged.UnitDir
if driveUnitDir == "" {
driveUnitDir = "/etc/systemd/system"
}
driveKnown := storage.NewRegistryKnownTargets(driveUnitDir, intentReader, logger)
// One-time migration: record pre-Impl-2a drives (mounted, with a Felhom unit) as enrolled so the
// registry-sourced Known() tracks them without their legacy PVE dir-storage. Idempotent.
if mounts, merr := hostReader.Mounts(); merr == nil {
storage.ReconcileExistingDrives(driveUnitDir, mounts, intentStore, logger)
} else {
logger.Warn("storage: existing-drive migration skipped — mount read failed", "err", merr)
}
watchdog := storage.NewWatchdog(storage.WatchdogOptions{
Targets: storage.NewCachingKnownTargets(observer, cfg.Storage.KnownRefresh()),
Targets: storage.NewCachingKnownTargets(driveKnown, cfg.Storage.KnownRefresh()),
Liveness: storage.NewHostLiveness(hostReader, 0),
Remounter: remounter,
Intent: intentReader, // P3: only re-mount enrolled drives
@@ -513,7 +528,7 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
jobsRunner := signedjobs.NewRunner(client, gate, signedjobs.ExecutorChain{wipeExec, decommExec}, cfg.Hub.HostID, logger)
loop.SetEnvelopeObserver(hub.MultiObserver(desiredSyncer, jobsRunner))
localSrv := buildLocalAPIServer(cfg, px, backupStore, observer, hostOps, gate, collector, intentRec, guestBindStore, formatJobStore, logger, &localTokens)
localSrv := buildLocalAPIServer(cfg, px, backupStore, observer, driveKnown, hostOps, gate, collector, intentRec, guestBindStore, formatJobStore, logger, &localTokens)
if localTokens != nil {
defer localTokens.Close()
}
@@ -706,7 +721,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.SudoHostOps, gate *reconcile.Gate, collector *hub.Collector, intent localapi.IntentRecorder, guestBinds *localapi.GuestBindStore, formatJobs *localapi.FormatJobStore, logger *slog.Logger, outTokens **localapi.TokenStore) *localapi.Server {
func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.Store, observer *storage.Observer, driveTargets storage.KnownTargets, hostOps *storage.SudoHostOps, gate *reconcile.Gate, collector *hub.Collector, intent localapi.IntentRecorder, guestBinds *localapi.GuestBindStore, formatJobs *localapi.FormatJobStore, logger *slog.Logger, outTokens **localapi.TokenStore) *localapi.Server {
if !cfg.LocalAPI.Enabled() {
return nil
}
@@ -751,6 +766,7 @@ func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.St
Backups: runner,
Store: store,
Storage: observer,
DriveTargets: driveTargets, // Impl-2a: registry+units drives for the /disks view (union w/ Observe storages)
Tokens: tokens,
BackupCadence: cfg.Backup.BackupCadence(),
// Disk management (slice 8C): the privileged host surface + the data-bearing wipe gate.