Physical-disk identity of every storage place + POST /disks/same-disk (felhom.eu architecture/16 §3/§4)
gates / gates (push) Successful in 31s

The agent names the physical disk(s) under each storage place from /sys, udev and /proc/mounts only
(partition -> disk, dm/LVM thin -> slaves, loop -> backing file), keys them (eui / serial:<bus>:<model>
:<serial> / vendor WWN) and says proven or unknown (vm, network, usb_bridge, weak_wwn, no_serial).
GET /disks and the hub report's storage_targets gain kind, physical_disks, identity,
identity_reason (golden updated; the hub copy changes in the same session). POST /disks/same-disk
maps two guest paths to places the agent itself made and answers {different, proven, reason};
an absent drive is unmapped, never the root disk. Tests on fixture trees for every spike case,
red-proofed. CHANGELOG entry follows the release (release-complete gate).

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-15 21:09:18 +02:00
parent d2053e7d47
commit c5daa9480d
13 changed files with 1219 additions and 1 deletions
+8 -1
View File
@@ -220,6 +220,10 @@ type Options struct {
// ROLE (it backs SystemDisks for the eject role-gate + the /disks role hints). OPTIONAL — when nil
// it defaults to the production *storage.ProcHostReader. Injectable so the role-gate is testable.
HostReader storage.HostReader
// Identity computes physical-disk identity (felhom.eu architecture/16-storage-physical-identity.md §3)
// for the /disks rows and POST /disks/same-disk. OPTIONAL — when nil, /disks carries no identity and
// same-disk answers 503 "not configured" (the controller then keeps its old st_dev rule).
Identity *storage.IdentityResolver
// HostMetrics serves GET /host/metrics (slice 9) — host-wide health (cpu%/mem/load/uptime/
// cpu-temp) + per-storage capacity, host-wide and token-authed (one-customer-per-host). When
// nil the endpoint reports "not configured" (host still reports/reconciles).
@@ -325,7 +329,8 @@ type Server struct {
// guestPowerSweeps counts completed guest-power sweeps, for the liveness observable. Touched only
// from GuestPowerTick, which the ticker calls serially.
guestPowerSweeps int
host storage.HostReader // role classification source (optional; defaults to ProcHostReader)
host storage.HostReader // role classification source (optional; defaults to ProcHostReader)
ident *storage.IdentityResolver // `16` §3 physical-disk identity (optional)
hostMetrics HostMetricsProvider // slice 9 (optional)
hostID string // slice 10B: for the data-bearing-format pending-op hint
@@ -480,6 +485,7 @@ func NewServer(o Options) (*Server, error) {
formatJobs: o.FormatJobs,
staleLock: o.StaleLock,
host: o.HostReader,
ident: o.Identity,
hostMetrics: o.HostMetrics,
hostID: o.HostID,
agentVersion: o.AgentVersion,
@@ -546,6 +552,7 @@ func (s *Server) Handler() http.Handler {
// Disk management (slice 8C) — self-scoped; format routes through the data-bearing classifier+gate.
mux.HandleFunc("GET /disks", s.withGuest(s.handleDisks))
mux.HandleFunc("GET /disks/candidates", s.withGuest(s.handleDiskCandidates))
mux.HandleFunc("POST /disks/same-disk", s.withGuest(s.handleSameDisk)) // `16` §4 (v0.157.0)
mux.HandleFunc("POST /disks/assign", s.withGuest(s.handleDiskAssign))
mux.HandleFunc("POST /disks/eject", s.withGuest(s.handleDiskEject))
mux.HandleFunc("POST /disks/decommission", s.withGuest(s.handleDiskDecommission))