F20-BUG2 + F9-reporting: /disks surfaces wipe_durable_id (gate scheme) + guest_attached
F20-BUG2: the /disks list only carried DurableID in the uuid: scheme (for /disks/assign), but the wipe gate resolves devices in the byid:/byuuid: scheme — so a customer confirming a wipe with the advertised id was refused (binding_mismatch). Added a shared s.deviceDurableID seam used by BOTH handleDisks (new DiskInfo.WipeDurableID) and the format gate, so the id the customer copies from the list is exactly the id the gate accepts. DurableID (uuid:) is unchanged (still feeds assign). F9 (reporting half): added DiskInfo.GuestAttached — whether the drive's namespace is actually bound into THIS guest's config (guestBoundPaths), distinct from mere host presence (State). This is the signal whose absence made the HDD look available when it wasn't attached, and resolves the F2 hdd_configured-vs-/disks disagreement. Tests: wipe_durable_id is the gate scheme + distinct from uuid:; the list's wipe id matches the gate's device-id binding (no mismatch); guest_attached true iff bound into the guest.
This commit is contained in:
@@ -103,6 +103,17 @@ type DiskInfo struct {
|
||||
// controller strips the "uuid:" prefix to get the fs UUID it passes to POST /disks/assign —
|
||||
// the only way the (de-privileged) controller can learn the mount key it cannot read itself.
|
||||
DurableID string `json:"durable_id,omitempty"`
|
||||
// WipeDurableID is the device's WIPE-binding durable id in the SAME scheme the format gate resolves
|
||||
// against (byid:<wwn>/byuuid:<uuid>). F20-BUG2: the customer must confirm a data-bearing wipe with
|
||||
// THIS id, not DurableID (uuid:) — passing the uuid: id was rejected as a binding_mismatch. "" when
|
||||
// the device has no durable identity (a wipe of it can't be bound anyway). Distinct from DurableID
|
||||
// (uuid:, used for /disks/assign) on purpose; both are derived from the agent's own device read.
|
||||
WipeDurableID string `json:"wipe_durable_id,omitempty"`
|
||||
// GuestAttached reports whether THIS guest (the token's vmid) actually has the drive's felhom-data
|
||||
// namespace bound into its config — i.e. the drive is usable IN the guest, not merely present on the
|
||||
// host. F9: host presence (State=attached) != guest-usable; this is the missing signal that made the
|
||||
// HDD look available when it wasn't bound. Only meaningful for user-data drives.
|
||||
GuestAttached bool `json:"guest_attached"`
|
||||
}
|
||||
|
||||
// handleDisks lists the host's drives + data-bearing flags (read-only/benign).
|
||||
@@ -119,16 +130,20 @@ func (s *Server) handleDisks(w http.ResponseWriter, r *http.Request, vmid int) {
|
||||
// Resolve the OS/system disks ONCE for this request — role classification is agent-authoritative
|
||||
// (the agent's own mount/topology read, never the caller's claim).
|
||||
sysDisks, sysKnown := storage.SystemDisks(s.hostReader())
|
||||
// F9: which host mount paths are actually BOUND into THIS guest's config (guest-usable, not just
|
||||
// host-present). A bind's mp= equals the guest path, which is the drive's host mount path (`where`).
|
||||
boundPaths := s.guestBoundPaths(r.Context(), vmid)
|
||||
out := make([]DiskInfo, 0, len(targets))
|
||||
for _, t := range targets {
|
||||
di := DiskInfo{
|
||||
Name: t.Name, Type: t.Type, State: t.State,
|
||||
BackingDevice: t.BackingDevice, MountPath: t.MountPath, Class: t.ClassHint,
|
||||
DurableID: t.DurableID,
|
||||
Role: string(storage.RoleForStorage(t.Type, t.BackingDevice, sysDisks, sysKnown)),
|
||||
TotalBytes: t.TotalBytes,
|
||||
UsedBytes: t.UsedBytes,
|
||||
UsedFraction: t.UsedFraction,
|
||||
DurableID: t.DurableID,
|
||||
Role: string(storage.RoleForStorage(t.Type, t.BackingDevice, sysDisks, sysKnown)),
|
||||
TotalBytes: t.TotalBytes,
|
||||
UsedBytes: t.UsedBytes,
|
||||
UsedFraction: t.UsedFraction,
|
||||
GuestAttached: t.MountPath != "" && boundPaths[t.MountPath],
|
||||
}
|
||||
// Inspect the backing device for the UI's data-bearing hint (the authoritative check
|
||||
// is re-run at format time on the actual device).
|
||||
@@ -140,6 +155,11 @@ func (s *Server) handleDisks(w http.ResponseWriter, r *http.Request, vmid int) {
|
||||
di.DataBearing = true // fail-safe
|
||||
di.DataReason = "could not inspect device"
|
||||
}
|
||||
// F20-BUG2: surface the gate-scheme wipe id (byid:/byuuid:) so a customer-confirmed wipe
|
||||
// binds with the id the gate accepts. Same seam the gate uses → guaranteed to match.
|
||||
if wid, werr := s.deviceDurableID(t.BackingDevice); werr == nil {
|
||||
di.WipeDurableID = wid
|
||||
}
|
||||
}
|
||||
out = append(out, di)
|
||||
}
|
||||
@@ -447,7 +467,7 @@ func (s *Server) handleDiskFormat(w http.ResponseWriter, r *http.Request, vmid i
|
||||
// never the caller's claim). The agent also re-resolves the device's durable id; the customer's
|
||||
// confirmation must bind to it.
|
||||
role := s.deviceRole(r.Context(), req.Device)
|
||||
deviceDurable, derr := storage.DeviceDurableID(req.Device)
|
||||
deviceDurable, derr := s.deviceDurableID(req.Device)
|
||||
if derr != nil {
|
||||
deviceDurable = "" // refusal still stands; binding/pending-op just lack the id
|
||||
}
|
||||
@@ -517,6 +537,30 @@ func (s *Server) handleDiskFormat(w http.ResponseWriter, r *http.Request, vmid i
|
||||
"device is system/backup-protected — format requires an operator signature ("+dec.Reason+")")
|
||||
}
|
||||
|
||||
// guestBoundPaths returns the set of guest mountpoint paths (the `mp=` of each entry in the guest's
|
||||
// config) — i.e. the host drives actually BOUND into the guest. F9: this is the guest-attached signal
|
||||
// (`GuestAttached`) that distinguishes a guest-usable drive from one merely present on the host. A bind
|
||||
// created by AttachBind has `mp=<where>` where `where` is the drive's host mount path, so a storage
|
||||
// target is guest-attached iff its MountPath is in this set. Best-effort: a config-read error yields an
|
||||
// empty set (reported as not-attached — the safe direction).
|
||||
func (s *Server) guestBoundPaths(ctx context.Context, vmid int) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
if s.guests == nil {
|
||||
return out
|
||||
}
|
||||
cfg, err := s.guests.GuestConfig(ctx, vmid)
|
||||
if err != nil {
|
||||
s.logger.Warn("local-api: guest-attached check — could not read guest config", "vmid", vmid, "err", err)
|
||||
return out
|
||||
}
|
||||
for _, spec := range cfg.MountPoints() {
|
||||
if _, mp, _ := parseMount(spec); mp != "" {
|
||||
out[mp] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// durableIDForMount resolves the durable-id of the storage mounted at `where` (from the agent's own
|
||||
// storage view) — the key the intent store records enroll/eject against. "" if not resolvable.
|
||||
func (s *Server) durableIDForMount(ctx context.Context, where string) string {
|
||||
|
||||
Reference in New Issue
Block a user