v0.40.0: third CT volume — SSD user-data (/mnt/sys_drive, mp1) baked + -sysdata-grow
Extends the OS/Docker-data split to a three-volume layout: rootfs + Docker-data
(mp0) + SSD user-data (mp1 @ /mnt/sys_drive, backup=1) = the controller's
system_data_path. Clears the controller's "not a separate drive" warning with
zero controller change (it already auto-discovers <sys_drive>/felhom-data and
warns via system.IsMountPoint; the mp reaches the container via the existing
-v /mnt:/mnt:rslave bind).
- build-golden.sh: --mp1 ...,mp=/mnt/sys_drive,backup=1 (env GOLDEN_SYSDATA_GB=8);
findmnt /mnt/sys_drive separate-mount guard + vzdump aborts if mp0 OR mp1 excluded.
- bringup.go: DefaultSysDataMount=mp1; BringUpSpec.{SysDataGrowGB,SysDataMount};
new "4c" online grow-only block mirroring the "4b" Docker-data grow.
- main.go: -sysdata-grow / -sysdata-mount flags wired into all three call sites.
- Tests: SysDataGrow (asserts ResizeLXC mp1 +42G) + SysDataGrowZeroNoResize.
- RUNBOOK extended to the three-volume layout (32 rootfs + 200 docker + 50 user-data).
Static CT volume, NOT an enrolled drive — never enrolls/ejects/decommissions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PsnU2ASocYrvzqE82YDYW
This commit is contained in:
@@ -43,6 +43,11 @@ const bringUpKind = "bring_up"
|
||||
// DefaultDataVolMount is the mpN slot the golden bakes the Docker-data volume (/var/lib/docker) at.
|
||||
const DefaultDataVolMount = "mp0"
|
||||
|
||||
// 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"
|
||||
|
||||
// configLockMaxAttempts bounds the F4 config-lock retry. configLockBackoff is a package var so
|
||||
// tests can shrink it (the production value gives PVE time to release its async config lock).
|
||||
const configLockMaxAttempts = 5
|
||||
@@ -81,6 +86,13 @@ 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 int
|
||||
// SysDataMount is the mpN slot of the golden's user-data volume to grow; "" → DefaultSysDataMount ("mp1").
|
||||
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
|
||||
BootTimeout time.Duration // 0 → DefaultBootTimeout; bounds the link-up liveness wait
|
||||
@@ -249,6 +261,26 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// Capture the post-reset MAC for the result (fresh for provision; archived for DR keep).
|
||||
if cfg2, err := e.api.GuestConfig(ctx, spec.VMID); err == nil {
|
||||
res.AssignedMAC = net0MAC(cfg2)
|
||||
|
||||
@@ -126,6 +126,64 @@ 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) {
|
||||
const vmid = 8051
|
||||
api := &fakeAPI{cfg: map[int]proxmox.GuestConfig{vmid: scratchCfg()}}
|
||||
e, _, q := newEngine(t, api, EmptyProvider{})
|
||||
defer q.Close()
|
||||
|
||||
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)
|
||||
})
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if !sawData || !sawSys {
|
||||
t.Errorf("want mp0 +240G AND mp1 +42G, got %+v", api.resizes)
|
||||
}
|
||||
}
|
||||
|
||||
// SysDataGrowGB=0 must issue NO mp1 resize (grow is an orthogonal knob; separateness comes from the
|
||||
// golden, not the grow).
|
||||
func TestRunBringUp_StorageSplit_SysDataGrowZeroNoResize(t *testing.T) {
|
||||
const vmid = 8052
|
||||
api := &fakeAPI{cfg: map[int]proxmox.GuestConfig{vmid: scratchCfg()}}
|
||||
e, _, q := newEngine(t, api, EmptyProvider{})
|
||||
defer q.Close()
|
||||
|
||||
res := e.RunBringUp(context.Background(), BringUpSpec{
|
||||
Mode: ModeProvision, Archive: "local:backup/golden.tar.zst", VMID: vmid,
|
||||
RestoreStorage: "local-lvm", Hostname: "felhom-prov-8052",
|
||||
SysDataGrowGB: 0, // no sys-data grow
|
||||
})
|
||||
if res.Err != nil || !res.Pass {
|
||||
t.Fatalf("provision must pass, got %+v", res)
|
||||
}
|
||||
for _, r := range api.resizes {
|
||||
if r.disk == "mp1" {
|
||||
t.Errorf("SysDataGrowGB=0 must NOT resize mp1, got %+v", api.resizes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunBringUp_CompensatingRollback(t *testing.T) {
|
||||
const vmid = 8000
|
||||
lockBackoffFast(t)
|
||||
|
||||
Reference in New Issue
Block a user