v0.112.0 — E-2: GET /disks flags the backup-target drive
Additive backup_target field, true for the drive backing the PRIMARY tier. The controller cannot work this out itself: settings.StoragePath.BackupTarget is customer INTENT, and on the two boxes migrated by hand in E-1 that intent was never recorded -- intent is empty while the drive really IS the target. Without this flag the absent-target alarm could not name the drive on exactly the boxes that currently have one. omitempty + false on an older agent, so an old controller degrades to the generic disconnect alarm rather than a wrong one. Test asserts the target IS flagged AND the non-target is NOT, as a pair -- a blanket true would satisfy a naive one-sided check.
This commit is contained in:
@@ -1,3 +1,16 @@
|
||||
## v0.112.0 — E-2: GET /disks flags the backup-target drive (2026-07-29)
|
||||
|
||||
Additive field `backup_target` on each `/disks` entry, true for the drive backing the PRIMARY tier.
|
||||
|
||||
**The controller cannot work this out for itself.** Its `settings.StoragePath.BackupTarget` is
|
||||
customer INTENT, and on the two boxes migrated by hand in E-1 that intent was never recorded — so
|
||||
intent is empty while the drive really is the target. Without this flag the absent-target alarm could
|
||||
not name the drive on exactly the boxes that currently have one. `omitempty` + false on an older
|
||||
agent, so an old controller degrades to the generic disconnect alarm rather than a wrong one.
|
||||
|
||||
Test asserts the target IS flagged **and** the non-target is NOT, as a pair — a blanket `true` would
|
||||
satisfy a naive one-sided check.
|
||||
|
||||
## v0.111.0 — E-2c: the backup-target drive can no longer be ejected out from under the backup (2026-07-29)
|
||||
|
||||
**A regression guard on a configuration that is live right now.** E-1 (2026-07-28) moved each demo
|
||||
|
||||
@@ -2,6 +2,7 @@ package localapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -106,3 +107,41 @@ func TestEjectStillAllowedOnANonTargetDrive(t *testing.T) {
|
||||
"it must block exactly the target, not harden the whole eject path; got: %s", rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// E-2 — GET /disks must FLAG the backup-target drive, because the controller cannot work it out.
|
||||
//
|
||||
// The controller's own StoragePath.BackupTarget is customer INTENT, and on a box migrated by hand
|
||||
// (E-1, both demo boxes) nobody ever assigned it — intent is empty while the drive really is the
|
||||
// target. Without this flag the absent-target alarm could not name the drive on exactly the boxes
|
||||
// that have one, which is the only place it currently matters.
|
||||
func TestDisksFlagsTheBackupTargetDrive(t *testing.T) {
|
||||
h := backupTargetServer(t)
|
||||
rr := do(t, h, http.MethodGet, "/disks", "A", "")
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("GET /disks = %d, body %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
body := rr.Body.String()
|
||||
// The target must be flagged and the non-target must not be — asserted as a pair, since a
|
||||
// blanket true would satisfy a naive "is it flagged?" check.
|
||||
var got struct {
|
||||
Data struct {
|
||||
Disks []struct {
|
||||
Name string `json:"name"`
|
||||
BackupTarget bool `json:"backup_target"`
|
||||
} `json:"disks"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(body), &got); err != nil {
|
||||
t.Fatalf("decode: %v (body %s)", err, body)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, d := range got.Data.Disks {
|
||||
seen[d.Name] = d.BackupTarget
|
||||
}
|
||||
if !seen["felhom-backup"] {
|
||||
t.Errorf("felhom-backup is the primary tier's storage but backup_target is false; body: %s", body)
|
||||
}
|
||||
if seen["spare-drive"] {
|
||||
t.Errorf("spare-drive is NOT the target but was flagged — a blanket true is not a signal; body: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,10 @@ type DiskInfo struct {
|
||||
// HDD look available when it wasn't bound. Only meaningful for user-data drives. LEGACY (per-drive
|
||||
// `pct set -mpN` model) — the intermediary model uses BoundUnderParent.
|
||||
GuestAttached bool `json:"guest_attached"`
|
||||
// BackupTarget (E-2) reports that this drive backs the PRIMARY whole-guest backup tier. Additive:
|
||||
// an older controller ignores it. It is the agent's answer, not the controller's intent flag —
|
||||
// on a hand-migrated box (E-1) intent is unset while the drive really is the target.
|
||||
BackupTarget bool `json:"backup_target,omitempty"`
|
||||
// GuestPath is the drive's STABLE in-guest path in the intermediary-mount model
|
||||
// (/mnt/felhom-drives/<name>). This is what the controller repoints HDD_PATH to and registers as the
|
||||
// storage path. Set for /mnt/<name> drives; "" otherwise. Distinct from MountPath (the RAW host PVE
|
||||
@@ -177,6 +181,9 @@ func (s *Server) handleDisks(w http.ResponseWriter, r *http.Request, vmid int) {
|
||||
// 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)
|
||||
// E-2: the PRIMARY tier's storage id — the whole-guest vzdump destination. "" when no tier
|
||||
// carries a target (the legacy single-tier shape), which correctly flags nothing.
|
||||
primaryTargetID := s.primaryTier().TargetID
|
||||
out := make([]DiskInfo, 0, len(targets))
|
||||
for _, t := range targets {
|
||||
di := DiskInfo{
|
||||
@@ -188,6 +195,12 @@ func (s *Server) handleDisks(w http.ResponseWriter, r *http.Request, vmid int) {
|
||||
UsedBytes: t.UsedBytes,
|
||||
UsedFraction: t.UsedFraction,
|
||||
GuestAttached: t.MountPath != "" && boundPaths[t.MountPath],
|
||||
// E-2: is THIS drive the whole-guest backup target? The agent is the only component that
|
||||
// can answer — the controller's own StoragePath.BackupTarget is customer INTENT, and on a
|
||||
// box migrated by hand (E-1) nobody ever assigned it, so intent is empty while the drive
|
||||
// really is the target. Reported here so the controller can name the drive in an
|
||||
// absent-target alarm and hide the destructive controls the agent would refuse anyway.
|
||||
BackupTarget: t.Name == primaryTargetID,
|
||||
}
|
||||
// Intermediary model: the stable in-guest path + whether felhom-data is bound under the parent.
|
||||
// Only user-data /mnt/<name> drives have a guest path (system/backup mounts never cross in).
|
||||
|
||||
Reference in New Issue
Block a user