Dedicated data disks: whole-disk longhorn/rancher volumes by stable by-id path #18

Merged
michal merged 11 commits from feat/dedicated-data-disks into main 2026-10-14 22:47:02 +00:00
Owner

Multi-disk support for the v2 layout, built for converting thebeast into a prod k8s-worker (1 TB OS + 2 TB longhorn + 2 TB rancher). Until now every generator was single-disk (ignoredisk --only-use=$DISK, one VG).

What's new

Shared layout (v2 only)

  • DataDisks { longhorn?, rancher? } — /dev/disk/by-id paths ONLY (nvme kernel names are not stable across boots on multi-NVMe hosts; a swapped name formats the wrong disk)
  • v2Layout() drops the LV whose mount a dedicated disk supplies
  • validateDataDisks() shared by the HTTP API, the websocket command handler and the CLI: by-id enforced, capability-gated (no longhorn disk on a control-plane-only node, no rancher disk without k3s), no double-booking one disk

Fedora kickstart (the only generator extended; dataDisks on ubuntu/vyos are rejected, never silently dropped)

  • %pre resolves each by-id path to tonight's kernel name; a missing disk fails loudly instead of continuing single-disk
  • Fresh disk → ONE xfs partition spanning it, labelled lab-longhorn / lab-rancher (--grow is correct here: consuming the disk is the point)
  • The label is on-disk truth, like the lab-schema-v2 VG tag: a reprovision rediscovers labelled disks with no flags and mounts them --noformat; a fresh install refuses to wipe or shadow a labelled disk it was not told about; a label on the wrong disk, or a data LV coexisting with a dedicated disk, aborts rather than guesses
  • --disk now also accepts a by-id path, and OS-disk auto-detect can never pick a data disk

Plumbing: InstallConfig.dataDisks, /api/install validation, labctl provision install --longhorn-disk/--rancher-disk, labd forwarding. Also fixes a latent bug: the labd command-install path silently dropped schemaVersion (an explicit --schema-version 2 on a legacy role became v1 on the far side).

Runbook: migration/THEBEAST-K8S-WORKER.md — disk map by serial, what still needs copying off the degraded (zero-redundancy) rpool, VLAN move, the exact labctl command, k8s0 join with the CI/CD taint.

Verification

  • 569 unit tests pass, 34 new (data-disks.test.ts: validator, layout, render, full /api/install → /ks plumbing)
  • tsc --build clean; no new lint errors in touched files (diffed against main)
  • VM-drilled 2026-10-14 ✅ (bastion/tests/integration/data-disks.test.ts, run on thebeast; evidence in migration/drill-evidence/data-disks-2026-10-14.txt): a real PXE install with 2 data disks lands the exact layout (no longhorn/rancher LVs, labelled whole-disk xfs on the serial-matched disks), and a flagless reprovision rediscovers both by label and remounts them unformatted — filesystem UUIDs identical before/after, marker files intact in all four preserved mounts. Harness grew dataDisks VM support, an SSH-timeout post-mortem (serial console + domstate in the failure), KEEP_VMS=1, and LAB_TEST_SSH_KEY (a passphrase-protected default key fails as a fake "machine never came up").

🤖 Generated with Claude Code

https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF

Multi-disk support for the v2 layout, built for converting **thebeast** into a prod `k8s-worker` (1 TB OS + 2 TB longhorn + 2 TB rancher). Until now every generator was single-disk (`ignoredisk --only-use=$DISK`, one VG). ## What's new **Shared layout (v2 only)** - `DataDisks { longhorn?, rancher? }` — `/dev/disk/by-id` paths ONLY (nvme kernel names are not stable across boots on multi-NVMe hosts; a swapped name formats the wrong disk) - `v2Layout()` drops the LV whose mount a dedicated disk supplies - `validateDataDisks()` shared by the HTTP API, the websocket command handler and the CLI: by-id enforced, capability-gated (no longhorn disk on a control-plane-only node, no rancher disk without k3s), no double-booking one disk **Fedora kickstart** (the only generator extended; `dataDisks` on ubuntu/vyos are *rejected*, never silently dropped) - `%pre` resolves each by-id path to tonight's kernel name; a missing disk fails loudly instead of continuing single-disk - Fresh disk → ONE xfs partition spanning it, labelled `lab-longhorn` / `lab-rancher` (`--grow` is correct here: consuming the disk is the point) - The label is on-disk truth, like the `lab-schema-v2` VG tag: a **reprovision rediscovers labelled disks with no flags** and mounts them `--noformat`; a fresh install **refuses to wipe or shadow** a labelled disk it was not told about; a label on the wrong disk, or a data LV coexisting with a dedicated disk, aborts rather than guesses - `--disk` now also accepts a by-id path, and OS-disk auto-detect can never pick a data disk **Plumbing**: `InstallConfig.dataDisks`, `/api/install` validation, `labctl provision install --longhorn-disk/--rancher-disk`, labd forwarding. Also fixes a latent bug: the labd `command-install` path silently **dropped `schemaVersion`** (an explicit `--schema-version 2` on a legacy role became v1 on the far side). **Runbook**: `migration/THEBEAST-K8S-WORKER.md` — disk map by serial, what still needs copying off the degraded (zero-redundancy) rpool, VLAN move, the exact `labctl` command, k8s0 join with the CI/CD taint. ## Verification - 569 unit tests pass, 34 new (`data-disks.test.ts`: validator, layout, render, full `/api/install → /ks` plumbing) - `tsc --build` clean; no new lint errors in touched files (diffed against main) - **VM-drilled 2026-10-14** ✅ (`bastion/tests/integration/data-disks.test.ts`, run on thebeast; evidence in `migration/drill-evidence/data-disks-2026-10-14.txt`): a real PXE install with 2 data disks lands the exact layout (no longhorn/rancher LVs, labelled whole-disk xfs on the serial-matched disks), and a **flagless reprovision rediscovers both by label and remounts them unformatted** — filesystem UUIDs identical before/after, marker files intact in all four preserved mounts. Harness grew `dataDisks` VM support, an SSH-timeout post-mortem (serial console + domstate in the failure), `KEEP_VMS=1`, and `LAB_TEST_SSH_KEY` (a passphrase-protected default key fails as a fake "machine never came up"). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal added 1 commit 2026-10-14 17:59:00 +00:00
dedicated data disks: whole-disk longhorn/rancher volumes, by stable by-id path
Some checks failed
CI/CD / typecheck (pull_request) Failing after 13s
CI/CD / test (pull_request) Failing after 10s
CI/CD / lint (pull_request) Failing after 24s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
66182c8cb8
A machine with spare NVMe bays (thebeast: 1TB OS + 2TB + 2TB) wants the
opposite trade from the fixed 250G/300G LVs that exist to FIT a 1TB disk:
give /var/lib/longhorn and /var/lib/rancher a whole disk each and keep the
OS VG small. Until now every generator was single-disk
(ignoredisk --only-use=$DISK, one VG) and had no way to say that.

New in the shared layout (v2 only):
- DataDisks { longhorn?, rancher? }: /dev/disk/by-id paths ONLY, because
  nvme0n1/nvme1n1 enumeration is not stable across boots on multi-NVMe
  hosts and a swapped kernel name formats the wrong disk.
- v2Layout() drops the LV whose mount a dedicated disk supplies.
- validateDataDisks() shared by HTTP API, websocket handler and CLI, so
  all three reject with the same words: by-id enforced, capability-gated
  (no longhorn disk on a control-plane-only node, no rancher disk without
  k3s), no double-booking one disk.

Fedora kickstart (the only generator extended; dataDisks on ubuntu/vyos
are rejected, not dropped):
- %pre resolves each by-id path to tonight's kernel name; a missing disk
  fails the install loudly instead of continuing single-disk.
- Fresh disk: ONE xfs partition spanning it, labelled lab-longhorn /
  lab-rancher. --grow is correct here -- consuming the disk is the point,
  unlike LVs competing inside a shared VG.
- The label is on-disk truth, exactly like the lab-schema-v2 VG tag:
  a reprovision rediscovers labelled disks via blkid with NO flags and
  mounts them --noformat; a fresh install refuses to wipe or shadow a
  labelled disk it was not told about; a label on the wrong disk, or a
  data LV coexisting with a dedicated disk, aborts rather than guesses.
- --disk now also accepts a by-id path (resolved in %pre), and auto-detect
  can never pick a data disk as the OS disk.

Plumbing: InstallConfig.dataDisks, POST /api/install validation,
labctl provision install --longhorn-disk/--rancher-disk, labd forwarding.
The labd command-install path also silently DROPPED schemaVersion (an
explicit --schema-version 2 on a legacy role became v1 on the far side);
it now travels with dataDisks in the protocol message.

migration/THEBEAST-K8S-WORKER.md is the concrete runbook: disk map by
serial, data still to copy off the degraded (zero-redundancy) rpool,
VLAN move, the exact labctl command, and the k8s0 join with CI/CD taint.

569 tests pass (34 new), tsc --build clean, no new lint errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal added 1 commit 2026-10-14 18:01:17 +00:00
CI: pin pnpm from bastion/package.json and generate the Prisma client
Some checks failed
CI/CD / lint (pull_request) Successful in 47s
CI/CD / typecheck (pull_request) Successful in 1m16s
CI/CD / test (pull_request) Failing after 1m15s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
ee171a5f53
Every job on main has been failing at setup: pnpm/action-setup@v4 reads
packageManager from the package.json at the REPO ROOT, and a job-level
working-directory default does not apply to `uses:` steps, so the pin in
bastion/package.json was never seen ("No pnpm version is specified").
Point the action at bastion/package.json so the version stays declared
in exactly one place.

Past setup, typecheck/test/build would then hit the next wall: labd's
generated Prisma client does not exist after a fresh install (no
postinstall hook, by design), and tsc --build fails on it. Run
`pnpm -C src/labd run db:generate` right after install in the jobs that
compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal added 1 commit 2026-10-14 18:04:44 +00:00
CI: install the external test validators in the test job
Some checks failed
CI/CD / typecheck (pull_request) Successful in 46s
CI/CD / lint (pull_request) Successful in 1m10s
CI/CD / test (pull_request) Failing after 46s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
b15eabe191
ubuntu-autoinstall.test.ts and layout-consistency.test.ts parse the
rendered autoinstall YAML with python3+yaml, and kickstart.test.ts runs
ksvalidator -- none of which the runner image ships. 35 of the 569 tests
failed on exactly that, the same set that fails on a bare workstation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal added 1 commit 2026-10-14 18:07:23 +00:00
CI: pykickstart comes from pip, not apt
Some checks failed
CI/CD / lint (pull_request) Successful in 45s
CI/CD / typecheck (pull_request) Successful in 1m10s
CI/CD / test (pull_request) Failing after 1m24s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
9bbaf2a385
The runner image's apt sources do not carry pykickstart
(E: Unable to locate package). python3-yaml stays apt; ksvalidator is
pip-installed system-wide so the test job finds it on PATH.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal added 1 commit 2026-10-14 18:10:53 +00:00
smoke test: surface stderr when the bastion banner never appears
Some checks failed
CI/CD / lint (pull_request) Successful in 48s
CI/CD / typecheck (pull_request) Successful in 47s
CI/CD / test (pull_request) Failing after 1m26s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
88848f04ac
The CI failure said only "expected '' to contain ...": the process died
before printing anything to stdout and the test swallowed stderr, which
is the half that says why.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal added 1 commit 2026-10-14 18:14:38 +00:00
CI: the runner image has no iproute2; the bastion smoke start needs ip route
Some checks failed
CI/CD / lint (pull_request) Successful in 46s
CI/CD / typecheck (pull_request) Successful in 1m13s
CI/CD / test (pull_request) Successful in 1m11s
CI/CD / build (pull_request) Failing after 1m5s
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
ddd80ce2ea
With stderr finally surfaced, the last failing test says it plainly:
detectInterface() shells out to `ip route` and the image does not ship it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal added 1 commit 2026-10-14 18:18:43 +00:00
CI: the nfpm sed pointed at a binary name that no longer exists
All checks were successful
CI/CD / typecheck (pull_request) Successful in 47s
CI/CD / lint (pull_request) Successful in 1m11s
CI/CD / test (pull_request) Successful in 1m11s
CI/CD / build (pull_request) Successful in 1m24s
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
b26733015f
nfpm.yaml ships ./dist/labctl; the workflow's sed rewrote
`src: ./dist/lab` -- a pattern that matches nothing -- so nfpm looked for
./dist/labctl, which the bun bundle steps never produce. This job had
never run before (every earlier run died at pnpm setup), so the drift
was invisible. Rewrite the pattern the file actually contains.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal added 1 commit 2026-10-14 21:20:58 +00:00
integration: VM drill for dedicated data disks
All checks were successful
CI/CD / typecheck (pull_request) Successful in 49s
CI/CD / lint (pull_request) Successful in 1m11s
CI/CD / test (pull_request) Successful in 1m14s
CI/CD / build (pull_request) Successful in 1m24s
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
16659236f2
One real-VM test mirroring thebeast's topology: OS disk + two data disks
addressed by /dev/disk/by-id/ (virtio serials standing in for NVMe
serials). Fresh k8s-worker v2 install must put labelled whole-disk xfs
on each data disk and drop the matching LVs; a flagless reprovision must
rediscover both by label and remount them unformatted -- proven by
marker files AND unchanged filesystem UUIDs.

Harness: createPxeVm grows dataDisks (extra virtio disks with fixed
serials, <=20 chars or the guest by-id link truncates), provisionVm
passes the by-id paths to /api/install, virtioById exported.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal added 1 commit 2026-10-14 21:46:35 +00:00
integration: post-mortem on SSH timeout (serial console + domstate), KEEP_VMS escape hatch
All checks were successful
CI/CD / lint (pull_request) Successful in 46s
CI/CD / typecheck (pull_request) Successful in 1m13s
CI/CD / test (pull_request) Successful in 1m12s
CI/CD / build (pull_request) Successful in 2m14s
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
8e00898c6f
A VM that installs fine but never reaches SSH was destroyed by afterAll
with the only evidence -- its console -- inside it. Now the failure
carries the last serial lines and domstate, and KEEP_VMS=1 preserves
the machine for inspection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal added 1 commit 2026-10-14 22:10:33 +00:00
integration: LAB_TEST_SSH_KEY for a dedicated passphrase-less test key
All checks were successful
CI/CD / typecheck (pull_request) Successful in 48s
CI/CD / lint (pull_request) Successful in 1m21s
CI/CD / test (pull_request) Successful in 1m13s
CI/CD / build (pull_request) Successful in 2m14s
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
745f9100c8
A passphrase-protected default id_rsa makes every provision test fail as
'SSH not available' with a healthy machine: BatchMode ssh silently never
offers the key without an agent, and non-interactive runs have no agent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal added 1 commit 2026-10-14 22:31:18 +00:00
thebeast runbook: VM drill passed, data copies done 2026-10-14
All checks were successful
CI/CD / lint (pull_request) Successful in 46s
CI/CD / typecheck (pull_request) Successful in 1m17s
CI/CD / test (pull_request) Successful in 1m12s
CI/CD / build (pull_request) Successful in 1m23s
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
6fe71eba63
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
michal merged commit 32d9b568ba into main 2026-10-14 22:47:02 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: michal/lab#18