91f6a26490
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>
118 lines
4.6 KiB
Go
118 lines
4.6 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
|
)
|
|
|
|
// RegistryKnownTargets sources the watchdog's known-drive set from the drive INTENT registry + the
|
|
// Felhom `.mount` units — NOT from PVE storages (Observe). Impl-2a decoupling: a drive tracked this way
|
|
// needs NO PVE dir-storage (which is what hid registry-only drives from health/detect, the 3b-fix class).
|
|
// Real PVE storages (local/local-lvm/pbs) are NOT drives and are handled elsewhere via Observe.
|
|
//
|
|
// A unit is included iff its drive's intent is not `new` (enrolled/ejected/decommissioned are all
|
|
// "known" = health-tracked; the watchdog's IntentReader gate then decides whether to RE-MOUNT — only
|
|
// `enrolled` is remounted). A `new`-intent unit (should not normally exist) is excluded so it never
|
|
// auto-mounts.
|
|
type RegistryKnownTargets struct {
|
|
unitDir string
|
|
intent IntentReader
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewRegistryKnownTargets builds the provider. unitDir is the systemd unit dir the Felhom `.mount`
|
|
// units live in (e.g. /etc/systemd/system); intent is the durable-id → intent store (may be nil →
|
|
// every Felhom unit is treated as known, matching the legacy ungated behaviour).
|
|
func NewRegistryKnownTargets(unitDir string, intent IntentReader, logger *slog.Logger) *RegistryKnownTargets {
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
return &RegistryKnownTargets{unitDir: unitDir, intent: intent, logger: logger}
|
|
}
|
|
|
|
// Known enumerates the Felhom drive units under unitDir and returns one KnownTarget per enrolled/
|
|
// tracked drive. It mirrors ReassertEnrolledMounts's enumeration (read dir → parseFelhomMountUnit).
|
|
func (r *RegistryKnownTargets) Known(ctx context.Context) ([]KnownTarget, error) {
|
|
entries, err := os.ReadDir(r.unitDir)
|
|
if err != nil {
|
|
return nil, err // let the caching layer keep the last good set; the watchdog logs + skips a cycle
|
|
}
|
|
var out []KnownTarget
|
|
for _, e := range entries {
|
|
if !strings.HasSuffix(e.Name(), ".mount") {
|
|
continue
|
|
}
|
|
data, rerr := os.ReadFile(filepath.Join(r.unitDir, e.Name()))
|
|
if rerr != nil {
|
|
continue
|
|
}
|
|
spec, ok := parseFelhomMountUnit(string(data))
|
|
if !ok {
|
|
continue // not one of our by-uuid drive units (netmount units are excluded by the parser)
|
|
}
|
|
durableID := "uuid:" + spec.UUID // the same scheme enroll records in the intent store
|
|
if r.intent != nil && r.intent.Get(durableID) == IntentNew {
|
|
// A unit with no recorded intent — not enrolled/ejected/decommissioned. Do NOT track it
|
|
// (and the watchdog must not auto-mount it): a genuine drive must be enrolled first.
|
|
continue
|
|
}
|
|
out = append(out, KnownTarget{
|
|
Name: spec.Name,
|
|
Type: hub.StorageTypeUSB, // a Felhom drive; MountBacked is what the watchdog keys on
|
|
DurableID: durableID,
|
|
UUID: spec.UUID,
|
|
MountBacked: true,
|
|
MountPath: spec.Where,
|
|
// BackingDevice left "" — HostLiveness resolves the device by UUID (by-uuid symlink).
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ReconcileExistingDrives migrates drives enrolled before Impl-2a into the intent-registry model so the
|
|
// registry-sourced Known() tracks them without depending on their (legacy) PVE dir-storage: for each
|
|
// Felhom `.mount` unit whose drive is currently mounted, ensure the intent registry records it
|
|
// `enrolled`. Idempotent (an already-enrolled drive is a no-op) and non-destructive — it creates no PVE
|
|
// storage and removes nothing. Best-effort per drive. Call once at agent start.
|
|
func ReconcileExistingDrives(unitDir string, mounts []Mount, intent *IntentStore, logger *slog.Logger) {
|
|
if intent == nil || logger == nil {
|
|
return
|
|
}
|
|
mounted := make(map[string]bool, len(mounts))
|
|
for _, m := range mounts {
|
|
mounted[m.MountPoint] = true
|
|
}
|
|
entries, err := os.ReadDir(unitDir)
|
|
if err != nil {
|
|
logger.Warn("migrate: cannot read unit dir — existing-drive reconcile skipped", "dir", unitDir, "err", err)
|
|
return
|
|
}
|
|
for _, e := range entries {
|
|
if !strings.HasSuffix(e.Name(), ".mount") {
|
|
continue
|
|
}
|
|
data, rerr := os.ReadFile(filepath.Join(unitDir, e.Name()))
|
|
if rerr != nil {
|
|
continue
|
|
}
|
|
spec, ok := parseFelhomMountUnit(string(data))
|
|
if !ok || !mounted[spec.Where] {
|
|
continue // only migrate currently-active Felhom drives
|
|
}
|
|
durableID := "uuid:" + spec.UUID
|
|
if intent.Get(durableID) != IntentNew {
|
|
continue // already tracked (enrolled/ejected/decommissioned) — idempotent no-op
|
|
}
|
|
if serr := intent.SetEnrolled(durableID); serr != nil {
|
|
logger.Warn("migrate: could not record enrolled intent", "drive", spec.Name, "err", serr)
|
|
} else {
|
|
logger.Info("migrate: recorded existing drive as enrolled", "drive", spec.Name, "durable_id", durableID)
|
|
}
|
|
}
|
|
}
|