Skip to content

feat: add lblk (non-NVMe block device) support for storage nodes - #437

Merged
noctarius merged 3 commits into
mainfrom
feat/lblk-block-device-support
Sep 15, 2026
Merged

noctarius merged 3 commits into
mainfrom
feat/lblk-block-device-support

Conversation

@boddumanohar

@boddumanohar boddumanohar commented Aug 17, 2026

Copy link
Copy Markdown
Member

fixes: #436

Exposes sbcli's lblk device mode as a cluster-level choice plus per-fleet device selectors, so storage nodes can run on generic Linux block devices — cloud block volumes, virtio-blk disks, SAS/SATA disks — instead of requiring NVMe PCIe hardware. In lblk mode each selected device is wrapped in an SPDK AIO bdev; everything from alceml upward is unchanged.

apiVersion: storage.simplyblock.io/v1alpha2
kind: StorageCluster
spec:
  deviceClass: LogicalBlock       # cluster-wide, immutable, defaults to NVMe
---
apiVersion: storage.simplyblock.io/v1alpha1
kind: StorageNodeSet
spec:
  enableLblk: true
  blkSerials: ["QEMU_HARDDISK_drive-scsi1", "QEMU_HARDDISK_drive-scsi2"]
  blkForceFormat: false          # destructive; reuses a partitioned disk
  lblkJournalPercent: 3          # only when the smallest selection is a partition
  • deviceClass (NVMe|LogicalBlock, default NVMe) is the cluster-wide choice, mapped onto sbcli's device_mode (nvme|lblk) on POST /api/v2/clusters/. Immutable — the backend bakes it in at cluster-create and cannot switch a live cluster. This field predates this PR: it shipped on StorageCluster v1alpha2 in feat(operator): StorageCluster moves to v1alpha2, and its operations are rebuilt #536, and this PR wires it through to cluster-create rather than adding a field of its own.
  • enableLblk turns a fleet onto block devices. Immutable, and mutually exclusive with the NVMe selectors.
  • blkNames / blkNamesExclude / blkSerials select the devices: by kernel name, by kernel name negatively, or by serial/WWN. At most one may be set; with none set, every eligible whole disk on the node is used. blkSerials is the one to prefer on cloud and virtualized disks, whose /dev/sdX name is not stable across reboots.
  • blkForceFormat wipes partition tables and filesystem signatures so a partitioned disk becomes eligible as a whole disk. Destructive, and off by default — without it a partitioned disk is simply rejected as ineligible.
  • lblkJournalPercent carves the journal out of the smallest selected device when that device is a partition rather than a whole disk. Ignored otherwise; the backend's own default is 3.

Every field exists on StorageNodeSet.spec, on its per-node nodeConfigs entries, and on StorageNode.spec.overrides, exactly like the existing PCIe selectors, and the most specific one wins.

How a selection reaches the node

Device selection is not part of the "add storage node" REST API — it happens node-locally in node_configure.py, which the DaemonSet init container already drives for NVMe. The lblk fields ride the same path rather than introducing a second integration point:

Field ConfigMap env node_configure.py flag
enableLblk LBLK --lblk
blkNames BLK_NAMES --blk-names
blkNamesExclude BLK_NAMES_EXCLUDE --blk-names-exclude
blkSerials BLK_SERIALS --blk-serials
blkForceFormat LBLK_FORCE_FORMAT --force-format
lblkJournalPercent LBLK_JM_PERCENT --jm-percent

The s-node-api-config-generator init container that runs node_configure.py now also mounts the host's /dev and /sys (already mounted on the main container, for SPDK). NVMe discovery never needed them: it resolves a device to a PCI address purely from /sys/class/nvme and /sys/bus/pci, sysfs metadata that's visible in any container without a hostPath mount. lblk devices have no such bus to walk — eligibility (mounted, held by LVM/md, partitioned, NUMA node) can only be read off the real block layer: lsblk over the actual device nodes, /sys/block/<name>/holders, and /dev/disk/by-id/* for stable identity. That by-id tree in particular only exists because the host's udev populates it; a container's own default /dev never has one, hostPath or not.

What admission rejects

Change Result
lblk selector and PCIe selector on one StorageNodeSet ❌ rejected — the backend rejects a node configured with both
blkNames/blkNamesExclude/blkSerials set on a new object ✅ allowed
the same fields edited after they are set ❌ rejected as immutable
deviceClass changed on a live cluster ❌ rejected as immutable (generated from StorageCluster's +k8s:immutable marker)

The immutability covers the unset-to-set transition too, which matters more than it looks: a fleet commonly starts with no selector at all (auto-select), and node_configure.py silently skips regenerating config for an already-provisioned node — so an edit that Kubernetes accepted would read back as though it had taken effect while changing nothing.

Scope

This PR is now field plumbing plus the two fixes specific to lblk itself (admission's immutability/mutual-exclusivity rules and the config-generator's /dev//sys mounts). Two other fixes found during the same config-israel testing pass but unrelated to lblk — a StorageNode deletion race when its parent StorageNodeSet is already gone, and the storage node ServiceAccount token's expiration — are split out to #535 rather than riding along here.

Rebase note (2026-09-15)

Rebased onto main past the StorageCluster v1alpha2 rebuild (#536). That
rebuild had already shipped spec.deviceClass: NVMe|LogicalBlock — the same
concept this PR originally added as its own spec.deviceMode on v1alpha1 —
and v1alpha1 is now a spoke converting into the v1alpha2 hub, so a
deviceMode field added only there would round-trip through the conversion
and be silently dropped. This PR now drops that field and instead wires the
existing deviceClass through to cluster-create in the new
storagecluster_controller.go. StorageNodeSet/StorageNode's lblk fields
are unaffected: those kinds have not moved to v1alpha2 yet.

🤖 Generated with Claude Code

@boddumanohar
boddumanohar force-pushed the feat/lblk-block-device-support branch 2 times, most recently from 63b7682 to d2953d2 Compare August 19, 2026 13:08
boddumanohar added a commit that referenced this pull request Aug 19, 2026
…d-disk reuse

blkForceFormat (StorageNodeSet fleet-wide + per-node override) threads
through two places lblk's --force-format actually matters:
- node_configure.py's --force-format flag (marks a partitioned disk
  eligible at configure time)
- the add-node API's force_format param (triggers the actual wipefs at
  add-node time — configure-time eligibility alone doesn't wipe anything)

Found while working through the lblk-e2e-testing.md test plan: PR #437
never wired this at all, so partitioned-disk reuse and the
disk+own-partition-conflict test were both unreachable through the CRD.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@noctarius noctarius added this to the 26.4 milestone Aug 20, 2026
boddumanohar added a commit that referenced this pull request Aug 21, 2026
…d-disk reuse

blkForceFormat (StorageNodeSet fleet-wide + per-node override) threads
through two places lblk's --force-format actually matters:
- node_configure.py's --force-format flag (marks a partitioned disk
  eligible at configure time)
- the add-node API's force_format param (triggers the actual wipefs at
  add-node time — configure-time eligibility alone doesn't wipe anything)

Found while working through the lblk-e2e-testing.md test plan: PR #437
never wired this at all, so partitioned-disk reuse and the
disk+own-partition-conflict test were both unreachable through the CRD.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@boddumanohar
boddumanohar force-pushed the feat/lblk-block-device-support branch from d551c39 to 6fb2971 Compare August 21, 2026 11:50
boddumanohar added a commit that referenced this pull request Aug 25, 2026
…d-disk reuse

blkForceFormat (StorageNodeSet fleet-wide + per-node override) threads
through two places lblk's --force-format actually matters:
- node_configure.py's --force-format flag (marks a partitioned disk
  eligible at configure time)
- the add-node API's force_format param (triggers the actual wipefs at
  add-node time — configure-time eligibility alone doesn't wipe anything)

Found while working through the lblk-e2e-testing.md test plan: PR #437
never wired this at all, so partitioned-disk reuse and the
disk+own-partition-conflict test were both unreachable through the CRD.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@boddumanohar
boddumanohar force-pushed the feat/lblk-block-device-support branch from e453622 to 5c53534 Compare August 25, 2026 11:43
@noctarius
noctarius force-pushed the main branch 2 times, most recently from 60dceb7 to fbaabe4 Compare September 9, 2026 10:21
@boddumanohar
boddumanohar force-pushed the feat/lblk-block-device-support branch 2 times, most recently from 11ccac8 to 3884341 Compare September 11, 2026 14:19
@boddumanohar
boddumanohar marked this pull request as ready for review September 11, 2026 14:29
geoffrey1330
geoffrey1330 previously approved these changes Sep 11, 2026
@boddumanohar
boddumanohar force-pushed the feat/lblk-block-device-support branch from 3884341 to 0b84391 Compare September 14, 2026 09:20
boddumanohar added a commit that referenced this pull request Sep 15, 2026
Storage node pods used Kubernetes' default auto-injected ServiceAccount
token, which lives roughly an hour. If simplyblock-storage-node-sa was
ever deleted and recreated (e.g. during a chart upgrade), every
already-running pod kept its stale token until the hour expired, then
started failing API calls with 401 — recoverable only by deleting every
pod by hand.

Pods now mount an explicit ten-minute projected token instead
(AutomountServiceAccountToken: false, with a kube-api-access-short
projected volume built to match the shape of Kubernetes' own
auto-injected one). kubelet refreshes a projected token at ~80% of its
expiration, so a ServiceAccount recreation now self-heals within
minutes.

Found while testing #437's lblk device-mode work on config-israel, but
unrelated to lblk itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
boddumanohar and others added 2 commits September 15, 2026 11:18
Wires the sbcli lblk device mode through the operator: a cluster-wide
StorageCluster.spec.deviceMode ("nvme" or "lblk") passed to cluster-create,
and fleet and per-node block-device selectors (enableLblk, blkNames,
blkNamesExclude, blkSerials, blkForceFormat, and lblkJournalPercent) on
StorageNodeSet and StorageNode that flow into the per-node ConfigMap and from
there into the DaemonSet init container's node_configure.py invocation
(--lblk, --blk-names, --blk-names-exclude, --blk-serials, --force-format, and
--jm-percent), mirroring the existing PCIe selector wiring.

Storage nodes can then run on cloud block volumes, virtio-blk disks, or other
non-NVMe local disks instead of requiring physical NVMe hardware.
Addresses #436.

Carries the fixes found while testing this on config-israel that are
specific to lblk itself:

- Admission enforces the immutability of blkNames, blkNamesExclude, and
  blkSerials, including the unset-to-set transition the backend silently
  ignores, and the mutual exclusivity of the lblk and PCIe selectors.
- The config-generator init container mounts /dev and /sys, which device
  discovery needs.
- --force-format reaches node_configure.py, so a partitioned disk can be
  reused as a whole-disk lblk device.

Two other fixes found during the same testing pass but unrelated to lblk
(a StorageNode deletion race when its parent StorageNodeSet is already gone,
and the storage node ServiceAccount token's expiration) are split out to a
separate PR rather than riding along here.

The backend side of this is on sbcli main: sending device_mode=lblk or the
lblk configure flags to an older control plane fails at cluster-create or
node-configure time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rebasing onto main after the StorageCluster v1alpha2 rebuild (#536) surfaced
that this branch's own spec.deviceMode field duplicated the deviceClass field
that rebuild already shipped (NVMe/LogicalBlock, immutable, defaulted to
NVMe), and that v1alpha1 is now a spoke converting into it — a bare deviceMode
addition there would have round-tripped through the hub and been silently
dropped. Dropping that field and instead mapping the existing
spec.deviceClass onto sbcli's cluster-create device_mode wire value in the new
storagecluster_controller.go keeps one field for the concept instead of two.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@boddumanohar
boddumanohar force-pushed the feat/lblk-block-device-support branch from 0b84391 to 92d1d05 Compare September 15, 2026 09:29
…ster.spec.deviceMode

The rebase's deviceClass wiring commit removed the v1alpha1 deviceMode field
from the Go type and config/crd/bases, but dist/install.yaml is kustomize
output rather than a controller-gen artifact and had to be rebuilt separately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@noctarius
noctarius merged commit f80fb67 into main Sep 15, 2026
17 checks passed
@noctarius
noctarius deleted the feat/lblk-block-device-support branch September 15, 2026 11:53
noctarius added a commit that referenced this pull request Sep 17, 2026
Brings the VolumeGroupSnapshot work (#504), lblk device support (#437), the
client-side compression design (#398), and two openapi.json syncs (#541, #549)
onto the branch carrying the CRD redesign.

Thirteen files conflicted. The eight generated ones — the deepcopy, the
StorageNode CRD in its four copies, the manager role, the chart's webhook
template, and install.yaml — were resolved by regenerating rather than by
editing, since a generated file has no side to prefer.

Three were modify-and-delete, where the lblk work edited the v1alpha1
controllers this branch retired: storagenode_controller.go, the per-node
ConfigMap writer, and the StorageNodeSet unit test. The deletions stand. Their
replacements under internal/controllers/node are what the operator runs, and
restoring a v1alpha1 controller to hold a feature would reinstate the model the
redesign removed.

cmd/main.go took both import sides.

One test came across orphaned. TestBuildStorageNodeSetDaemonSet_ConfigGenerator-
MountsDevAndSys guards the /dev and /sys mounts node_configure.py's lblk
eligibility check needs, and it was written against the v1alpha1 builder; it is
rewritten against BuildStorageNodeDaemonSet, which is the builder this operator
runs and which carries those mounts. The assertion is about the container rather
than about which kind describes it.

lblk arrives half-wired, and this commit does not finish it. What works is the
cluster half and the workload half: StorageCluster.spec.deviceClass still
resolves to sbcli's device_mode, and the DaemonSet's init script still assembles
--lblk, --blk-names, --blk-names-exclude, --blk-serials, --jm-percent, and
--force-format. What is missing is the field half. The six selectors exist on
v1alpha1's StorageNodeSet and StorageNode, which this operator no longer reads,
and have no counterpart on v1alpha2; the per-node ConfigMap writer under
internal/controllers/node therefore emits none of the LBLK or BLK_ keys the init
script reads, so every flag resolves empty. Selecting devices by lblk is inert
on this branch until v1alpha2 carries the fields, which is its own change rather
than a conflict resolution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support non-NVMe (generic block device) storage nodes

3 participants