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:
@@ -27,6 +27,9 @@ type DiskOps interface {
|
||||
Unmount(ctx context.Context, where string) error
|
||||
Format(ctx context.Context, device, fstype string) error
|
||||
InspectDevice(ctx context.Context, device string) (storage.DeviceProbe, error)
|
||||
// ListCandidateDisks enumerates host disks the Impl-1 unclaimed filter proves are free to enroll
|
||||
// (Impl-2a discovery). Fail-safe: a device not provably unclaimed is omitted.
|
||||
ListCandidateDisks(ctx context.Context) ([]storage.CandidateDisk, error)
|
||||
}
|
||||
|
||||
// StorageGate authorizes a DESTRUCTIVE storage op (a data-bearing wipe/format) through the
|
||||
@@ -205,6 +208,33 @@ func (s *Server) handleDisks(w http.ResponseWriter, r *http.Request, vmid int) {
|
||||
}
|
||||
out = append(out, di)
|
||||
}
|
||||
// Impl-2a: union in registry+units drives that Observe() does NOT surface (a drive with no PVE
|
||||
// dir-storage). Additive + deduped by mount path — an existing drive already shown via Observe is
|
||||
// NOT duplicated, and no Observe row is dropped (so this can never regress the current view).
|
||||
if s.driveTargets != nil {
|
||||
seen := make(map[string]bool, len(out))
|
||||
for _, d := range out {
|
||||
if d.MountPath != "" {
|
||||
seen[d.MountPath] = true
|
||||
}
|
||||
}
|
||||
if drives, derr := s.driveTargets.Known(r.Context()); derr == nil {
|
||||
for _, d := range drives {
|
||||
if d.MountPath == "" || seen[d.MountPath] {
|
||||
continue
|
||||
}
|
||||
out = append(out, DiskInfo{
|
||||
Name: d.Name, Type: d.Type, State: "attached",
|
||||
MountPath: d.MountPath, DurableID: d.DurableID,
|
||||
Role: string(storage.RoleUserData),
|
||||
GuestAttached: boundPaths[d.MountPath],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
s.logger.Warn("disks: registry drive union skipped", "err", derr)
|
||||
}
|
||||
}
|
||||
|
||||
// Guest boot-id (intermediary model): changes on every guest boot, stable across controller restarts.
|
||||
// The controller persists it and deterministically recreates drive-backed apps when it changes.
|
||||
bootID := ""
|
||||
@@ -214,6 +244,31 @@ func (s *Server) handleDisks(w http.ResponseWriter, r *http.Request, vmid int) {
|
||||
writeOK(w, map[string]any{"vmid": vmid, "disks": out, "guest_boot_id": bootID})
|
||||
}
|
||||
|
||||
// handleDiskCandidates (Impl-2a) lists the host disks FREE for Felhom to enroll — the raw-device
|
||||
// discovery the enrollment wizard (Impl-2b) consumes. Read-only + benign (the Impl-1 filter never
|
||||
// offers a claimed/OS disk). Response splits into `initialize` (all unclaimed) and `attach` (the subset
|
||||
// already carrying a mountable FS). GET /disks/candidates.
|
||||
func (s *Server) handleDiskCandidates(w http.ResponseWriter, r *http.Request, vmid int) {
|
||||
if s.disks == nil {
|
||||
writeErr(w, http.StatusServiceUnavailable, "disk management not configured on this host")
|
||||
return
|
||||
}
|
||||
cands, err := s.disks.ListCandidateDisks(r.Context())
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadGateway, "could not scan host disks")
|
||||
return
|
||||
}
|
||||
initialize := make([]storage.CandidateDisk, 0, len(cands))
|
||||
attach := make([]storage.CandidateDisk, 0)
|
||||
for _, c := range cands {
|
||||
initialize = append(initialize, c) // every unclaimed disk can be initialized (format → enroll)
|
||||
if c.Mountable {
|
||||
attach = append(attach, c) // already has a mountable FS → attach without formatting
|
||||
}
|
||||
}
|
||||
writeOK(w, map[string]any{"vmid": vmid, "initialize": initialize, "attach": attach})
|
||||
}
|
||||
|
||||
type assignRequest struct {
|
||||
VMID int `json:"vmid"`
|
||||
UUID string `json:"uuid"`
|
||||
|
||||
Reference in New Issue
Block a user