build-golden.sh 2.1.0 -> 3.0.0: a layout change is a major. The golden ships ONE data volume at a NEUTRAL path (/var/lib/felhom); both /var/lib/docker and /mnt/sys_drive are binds of subdirectories of it. mp1 is gone. The variant was chosen on measurement. Three candidates were built and rebooted (SPIKE-r165-phase0-2026-08-03.md); all three boot, reboot 3/3, give ONE df figure and keep a container's statfs(/) on the merged volume — the ordering worry that motivated the probe did not materialise. They differ only in which guarantee they break: volume-at-docker puts customer backups inside Docker's data-root; volume-at-sys_drive puts Docker's ENTIRE data-root under /mnt, which the controller container mounts wholesale (measured: it then sees /mnt/sys_drive/docker). V-c breaks neither. The four assertions were RETARGETED, never deleted, and each was RUN against a deliberately wrong shape — a real split guest and a real archive of it: 8 checks, 8 passed. A new 2b asserts both paths are ONE filesystem, which catches the S2 shape the spike ranked worse than the split. Assertion 5 replaces the old "was mp1 excluded?" guard, whose pattern could no longer match — a guard that cannot match has silently stopped guarding. Provisioning: one volume, one grow. SysDataGrowGB is FOLDED IN rather than dropped, because a census established that felhom-host-install.sh passes -sysdata-grow and the two do not upgrade in the same instant; dropping it would silently shrink every appliance by 42 of 250 GiB. The flags stay accepted for the same reason. The existing test was retargeted to pin the fold, and it caught the change before I did.
This commit is contained in:
@@ -51,10 +51,15 @@ const DefaultDataVolMount = "mp0"
|
||||
// Single source of truth for both restore sites (provision bring-up + restore-test).
|
||||
const DefaultPool = "felhom"
|
||||
|
||||
// DefaultSysDataMount is the mpN slot the golden bakes the SSD user-data volume (/mnt/sys_drive) at.
|
||||
// This is the controller's system_data_path; provision grows it (SysDataGrowGB) like the Docker-data
|
||||
// volume. mp1 is the natural next bring-up slot (mp8/mp9 are added by the provision back-half).
|
||||
const DefaultSysDataMount = "mp1"
|
||||
// DefaultSysDataMount is RETIRED (agent v0.120.0, R-165 / decision D-a). The golden no longer bakes a
|
||||
// second volume: since build-golden.sh v3.0.0 there is ONE data volume at /var/lib/felhom (mp0) and
|
||||
// both /var/lib/docker and /mnt/sys_drive are binds of subdirectories of it, so there is no mp1 to
|
||||
// resize. The constant is kept, and deliberately points at nothing, so that a stale caller fails
|
||||
// loudly at review rather than silently resizing a slot that does not exist.
|
||||
//
|
||||
// SysDataGrowGB itself is NOT removed — see its field comment: the host installer still passes
|
||||
// `-sysdata-grow`, and its GiB are FOLDED INTO the single volume's grow rather than dropped.
|
||||
const DefaultSysDataMount = ""
|
||||
|
||||
// Structural host-bind mountpoints every provisioned guest carries (GL-5; verdict of
|
||||
// SPIKE-dr-bindmount-source-2026-07-07): the permanent drives parent bind (mp8,
|
||||
@@ -183,12 +188,18 @@ type BringUpSpec struct {
|
||||
DataVolGrowGB int
|
||||
// DataVolMount is the mpN slot of the golden's Docker-data volume to grow; "" → DefaultDataVolMount ("mp0").
|
||||
DataVolMount string
|
||||
// SysDataGrowGB grows the golden-carried SSD user-data volume (SysDataMount, default mp1, mounted at
|
||||
// /mnt/sys_drive = the controller's system_data_path) to the per-customer target. Same online,
|
||||
// grow-only mechanism as DataVolGrowGB. 0 = skip (keep the golden's small size — the volume is still
|
||||
// a separate mount, so the controller's "not a separate drive" warning clears regardless of grow).
|
||||
// SysDataGrowGB is a COMPATIBILITY INPUT since agent v0.120.0 (R-165). There is no longer a second
|
||||
// volume to grow — but `felhom.eu/scripts/felhom-host-install.sh` computes and passes
|
||||
// `-sysdata-grow` (its step_grows derives both numbers from the thin pool's free space), and an
|
||||
// installer and an agent do not upgrade in the same instant.
|
||||
//
|
||||
// SO ITS GiB ARE FOLDED INTO THE SINGLE VOLUME'S GROW RATHER THAN DROPPED. Dropping them would
|
||||
// silently shrink every appliance by the user-data share — on the ≥300 GiB branch that is 42 of
|
||||
// 250 GiB — which is exactly the "a knob that silently does nothing" outcome R-165 was told to
|
||||
// avoid. Folding keeps total capacity identical whichever installer version runs.
|
||||
SysDataGrowGB int
|
||||
// SysDataMount is the mpN slot of the golden's user-data volume to grow; "" → DefaultSysDataMount ("mp1").
|
||||
// SysDataMount is RETIRED and ignored (see DefaultSysDataMount). Kept so an older caller still
|
||||
// compiles; it selects nothing.
|
||||
SysDataMount string
|
||||
Mounts []GuestMount // additive mpN mounts (slice 7 may pass empty/test)
|
||||
KeepMAC bool // DR knob: keep the archived MAC (true) unless a source may be live
|
||||
@@ -407,12 +418,20 @@ func (e *Engine) runBringUp(ctx context.Context, spec BringUpSpec, res *BringUpR
|
||||
// online (storage-split B4); its OWN call like the rootfs resize. The volume + baked images
|
||||
// came in with the restore, so we grow it rather than attach a fresh one that would shadow
|
||||
// the baked images.
|
||||
if spec.DataVolGrowGB > 0 {
|
||||
//
|
||||
// R-165: ONE volume, therefore ONE grow. `SysDataGrowGB` is FOLDED IN here rather than driving
|
||||
// a second resize — see its field comment. This is the only arithmetic the merge added.
|
||||
growGB := spec.DataVolGrowGB + spec.SysDataGrowGB
|
||||
if growGB > 0 {
|
||||
mount := spec.DataVolMount
|
||||
if mount == "" {
|
||||
mount = DefaultDataVolMount
|
||||
}
|
||||
dupid, err := e.api.ResizeLXC(ctx, spec.VMID, mount, fmt.Sprintf("+%dG", spec.DataVolGrowGB))
|
||||
if spec.SysDataGrowGB > 0 {
|
||||
e.logger.Info("bring-up: folding the retired sys-data grow into the single data volume (R-165)",
|
||||
"data_grow_gb", spec.DataVolGrowGB, "sysdata_grow_gb", spec.SysDataGrowGB, "total_gb", growGB, "mount", mount)
|
||||
}
|
||||
dupid, err := e.api.ResizeLXC(ctx, spec.VMID, mount, fmt.Sprintf("+%dG", growGB))
|
||||
if err != nil {
|
||||
res.Err = fmt.Errorf("reconcile: bring-up data-volume resize (%s): %w", mount, err)
|
||||
return
|
||||
@@ -423,25 +442,9 @@ func (e *Engine) runBringUp(ctx context.Context, spec BringUpSpec, res *BringUpR
|
||||
}
|
||||
}
|
||||
|
||||
// 4c. Grow the golden-carried SSD user-data volume (mp1, /mnt/sys_drive = the controller's
|
||||
// system_data_path) to the per-customer target. Same shape as the Docker-data grow: grow-only,
|
||||
// online, its OWN call. The volume came in with the restore (separate mount, backup=1), so we
|
||||
// grow it rather than attach a fresh one.
|
||||
if spec.SysDataGrowGB > 0 {
|
||||
mount := spec.SysDataMount
|
||||
if mount == "" {
|
||||
mount = DefaultSysDataMount
|
||||
}
|
||||
supid, err := e.api.ResizeLXC(ctx, spec.VMID, mount, fmt.Sprintf("+%dG", spec.SysDataGrowGB))
|
||||
if err != nil {
|
||||
res.Err = fmt.Errorf("reconcile: bring-up sys-data resize (%s): %w", mount, err)
|
||||
return
|
||||
}
|
||||
if _, err := e.waitTask(ctx, supid, proxmox.WaitOptions{}); err != nil {
|
||||
res.Err = fmt.Errorf("reconcile: bring-up sys-data resize task (%s): %w", mount, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
// 4c. RETIRED (R-165). There is no second volume: the golden ships ONE, and the sys-data grow is
|
||||
// folded into 4b above. Deliberately left as a comment rather than silently vanishing, so a
|
||||
// reader of a v0.119.0 archive's provision log can see where the second resize went.
|
||||
|
||||
// 4d. DR structural-bind swap (GL-5): replace the two restore-time throwaway volumes (see the
|
||||
// restore call) with the REAL host binds, then delete the displaced volumes so a KEPT DR
|
||||
|
||||
@@ -265,10 +265,16 @@ func TestRunBringUp_StorageSplit_DataVolGrow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The golden-carried SSD user-data volume (/mnt/sys_drive) is grown via a SEPARATE resize on its
|
||||
// mpN slot (mp1), independent of the rootfs and Docker-data grows. With SysDataGrowGB=0 NO mp1
|
||||
// resize is issued (the volume stays at the golden size, still a separate mount).
|
||||
func TestRunBringUp_StorageSplit_SysDataGrow(t *testing.T) {
|
||||
// R-165 RETARGETED THIS TEST, and the retarget IS the contract change. There is no longer a second
|
||||
// volume, so `SysDataGrowGB` no longer drives its own resize on mp1 — its GiB are FOLDED INTO the
|
||||
// single volume's grow.
|
||||
//
|
||||
// FOLDED, NOT DROPPED, and that is the whole point. `felhom-host-install.sh` computes and passes
|
||||
// `-sysdata-grow` from the thin pool's free space, and an installer and an agent do not upgrade in
|
||||
// the same instant. Dropping the value would silently shrink every appliance built by an older
|
||||
// installer by the user-data share — 42 of 250 GiB on the standard branch — which is precisely the
|
||||
// "a knob that silently does nothing" outcome this work was told to avoid.
|
||||
func TestRunBringUp_StorageSplit_SysDataGrowIsFoldedIn(t *testing.T) {
|
||||
const vmid = 8051
|
||||
api := &fakeAPI{cfg: map[int]proxmox.GuestConfig{vmid: scratchCfg()}}
|
||||
e, _, q := newEngine(t, api, EmptyProvider{})
|
||||
@@ -277,26 +283,29 @@ func TestRunBringUp_StorageSplit_SysDataGrow(t *testing.T) {
|
||||
res := e.RunBringUp(context.Background(), BringUpSpec{
|
||||
Mode: ModeProvision, Archive: "local:backup/golden.tar.zst", VMID: vmid,
|
||||
RestoreStorage: "local-lvm", Hostname: "felhom-prov-8051",
|
||||
DataVolGrowGB: 240, SysDataGrowGB: 42, // grows mp0 AND mp1 (DefaultSysDataMount)
|
||||
DataVolGrowGB: 240, SysDataGrowGB: 42, // ONE volume: 240 + 42 = 282
|
||||
})
|
||||
if res.Err != nil || !res.Pass {
|
||||
t.Fatalf("provision must pass, got %+v", res)
|
||||
}
|
||||
// TWO resizes here: Docker-data mp0 +240G and the user-data volume mp1 +42G (no rootfs grow).
|
||||
if len(api.resizes) != 2 {
|
||||
t.Fatalf("expected data-volume + sys-data resizes, got %+v", api.resizes)
|
||||
// EXACTLY ONE resize. A second one would mean an mp1 the golden no longer ships.
|
||||
if len(api.resizes) != 1 {
|
||||
t.Fatalf("expected exactly ONE data-volume resize (there is no mp1 since R-165), got %+v", api.resizes)
|
||||
}
|
||||
var sawData, sawSys bool
|
||||
for _, r := range api.resizes {
|
||||
if r.disk == "mp0" && r.size == "+240G" {
|
||||
sawData = true
|
||||
}
|
||||
if r.disk == "mp1" && r.size == "+42G" {
|
||||
sawSys = true
|
||||
}
|
||||
r := api.resizes[0]
|
||||
if r.disk != "mp0" {
|
||||
t.Fatalf("resized %q, want mp0 — the single data volume", r.disk)
|
||||
}
|
||||
if !sawData || !sawSys {
|
||||
t.Errorf("want mp0 +240G AND mp1 +42G, got %+v", api.resizes)
|
||||
if r.size != "+282G" {
|
||||
t.Fatalf("resized %s, want +282G (240 data + 42 folded sys-data). Anything less means the "+
|
||||
"retired knob's GiB were DROPPED, silently shrinking every appliance an older "+
|
||||
"felhom-host-install.sh provisions", r.size)
|
||||
}
|
||||
for _, rr := range api.resizes {
|
||||
if rr.disk == "mp1" {
|
||||
t.Fatalf("an mp1 resize was issued (%+v) — the golden ships no second volume, so this "+
|
||||
"would fail on a real box", rr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user