fix(csi-driver): never format a device the plugin could not read - #482
fix(csi-driver): never format a device the plugin could not read#482boddumanohar wants to merge 4 commits into
Conversation
NodeStageVolume handed every cold-staged device to mount-utils' SafeFormatAndMount, which probes with blkid and cannot tell a device carrying no filesystem from one whose reads failed. blkid exits 2 for both, mount-utils maps that single exit code to "unformatted," and mkfs.ext4 -F -m0 runs. A volume behind a degraded NVMe-oF path, whose reads time out under nvme_core.io_timeout or whose controller has passed its ctrl_loss_tmo, was therefore reformatted rather than staged, and a production cluster lost a volume's data that way. Verified on a live host rather than argued from the source: an ext4 filesystem under a dm-flakey error_reads table, where reads fail and writes still land, makes blkid exit 2 with empty output. mkfs then discards the device's blocks and fails, leaving no valid filesystem at all, or, if the path recovers in between, succeeds and leaves the workload a silently empty volume. Take the decision away from mount-utils. atlas-lib/blockfs reads the start of the device and reports what it found, keeping "read successfully, found nothing" apart from "could not read," and formatAndMount formats only on the first. A device carrying a filesystem is mounted as it is, one carrying a foreign signature or one that will not answer a read fails the stage, and only a device proven to be all zeros reaches mkfs. mount-utils still performs the format, so the flags, the xfs feature pinning, and the stripe geometry are unchanged. Refusing to stage is the deliberate trade: an outage is recoverable and a wiped volume is not. Upstream tracks the same defect, reached through a corrupted primary superblock rather than an unreadable device, as kubernetes/kubernetes#140376, open and untriaged since 2026-07-09. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and back-fill it The claim annotation storage.simplyblock.io/on-disk-filesystem tells the node plugin what a volume already holds, so it can mount rather than format when the device itself will not answer. Two things stand between that mechanism and working on a real cluster. The node plugin cannot write the annotation. Its ClusterRole grants get, list, and watch on persistentvolumeclaims and nothing more, so the patch that records the filesystem is refused. The refusal is logged and swallowed, by design, which means the annotation silently never appears and the volume it was meant to protect stays exposed. Both charts that grant the node plugin any claim access now grant patch as well, with what needs it stated on the rule. The raw manifests under csi-driver/deploy/kubernetes grant no claim access at all, so that deployment path cannot use the annotation either way and is left alone. Nothing back-fills the annotation for volumes that already exist. The plugin records it from a volume's first successful stage onward, so a claim provisioned by an older driver has nothing recorded until then, and a stage that cannot read its device in the meantime has nothing to fall back on. The new script annotates the fleet in one pass. It writes only for a claim a running pod is using, which is the evidence that matters, since a volume mounted into a running pod has been formatted while a merely bound claim may never have been staged. The filesystem recorded is the one the claim's StorageClass asks for, which is what the driver formatted it with. Raw block volumes, claims that are not bound, claims of other provisioners, and claims that already record a filesystem are all skipped, and the script reports what it would do until it is given --apply. Verified against a four-node cluster: 6 simplyblock claims in use identified and their filesystem read correctly from the StorageClass, 8 local-path claims, one pending claim, and one raw block volume all skipped for the right reason, and a throwaway claim annotated, re-read, and skipped on a second run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed two commits that support the annotation mechanism in #481 rather than the probe in this PR, because both are needed for that mechanism to work on a real cluster and neither is in #481 today. Happy to move them there instead — say the word and I'll open them as a separate PR against The node plugin cannot write the annotation. Both charts that grant the node plugin claim access now grant Nothing back-fills existing volumes. Verified against a four-node cluster: 6 simplyblock claims in use identified with the right filesystem, 8 local-path claims, one pending claim, and one raw block volume skipped for the right reason; a throwaway claim annotated, re-read, and skipped on a rerun. Correction to what I wrote earlier in this PRI said the annotation "leaves two paths open," which understated it in one direction and overstated it in another, and I have since tested both. It is better than I said: the plugin writes the annotation on every successful stage, not only for newly provisioned volumes. An existing ext4 volume whose It is also narrower than I implied. The exposure is the window between deploying #481 and each volume's next successful stage — not the fleet indefinitely. What stays open is a volume that meets an unreadable device on its first stage after the upgrade (a node reboot is both the commonest cause of a cold stage and a moment when paths flap, so this ordering is not contrived), a claim recreated or restored without its annotation, and any stage where the claim cannot be read. Tested on this branch, against a real ext4 filesystem under a
and the same device, same failing reads, no annotation anywhere, on this PR: The two remain complementary. The annotation is a real feature and a cheap second opinion; the probe is what holds when no record exists yet. |
…y a loop device The test plan said a real NVMe namespace could not be made to fail reads. It can, and the recipe was wrong about why: a degraded path presents three ways, and which one a reproduction reaches decides whether anything is destroyed. Within ctrl_loss_tmo the kernel queues I/O, so the device stays present, blkid blocks, and staging hangs rather than formatting. Past ctrl_loss_tmo the controllers are deleted and the device node disappears, so blkid exits 2 but mkfs then hits a missing path. Only fast_io_fail_tmo reaches the production case: the controller keeps reconnecting while I/O fails immediately, so the namespace stays present and unreadable. Adds that recipe, run against a real simplyblock volume with two HA paths: the probe returns exit 2 with the device present and its ext4 intact, and once the path recovers mkfs exits 0 and the volume comes back holding nothing but lost+found. CONFIG_FAULT_INJECTION is not required. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rageClass The back-fill decided whether a claim was simplyblock's by looking up its StorageClass and checking the provisioner. That reads a fact about the class today to answer a question about a volume provisioned in the past, and the two come apart: a class can be renamed, deleted, or have its fstype parameter edited after the volumes it created were bound. A volume whose class is gone would be skipped, and one whose class changed filesystem since would be annotated with a filesystem that was never on it, which is worse than skipping it. The PersistentVolume answers both questions per volume and cannot go stale. spec.csi.driver names this driver, and spec.csi.fsType is what the volume was actually provisioned with. The StorageClass parameter is kept only as the fallback for a volume that records no filesystem of its own. Verified against a four-node cluster: the same 6 simplyblock claims are matched with the same filesystem, and the 8 local-path claims are now skipped as "not a simplyblock volume" rather than "not a simplyblock class". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
60dceb7 to
fbaabe4
Compare
The bug
NodeStageVolumehands every cold-staged device to mount-utils'SafeFormatAndMount, which probes withblkidand cannot tell a device carryingno filesystem from one whose reads failed.
blkidexits 2 for both,getDiskFormatmaps that single exit code to("", nil), andmkfs.ext4 -F -m0runs. A volume behind a degraded NVMe-oF path — reads timing out under
nvme_core.io_timeout, or a controller past itsctrl_loss_tmo— is thereforereformatted rather than staged. That is how a production cluster lost a volume.
The repair paths were already written against this hazard (
restageVolumeandhealVolumeBeforePublishuse a plainMounton purpose). Only the cold stagewas exposed, which is every node reboot, pod reschedule, failover clone, and
migrated volume.
Reproduced, not argued
On a live host, an ext4 filesystem under a
dm-flakeyerror_readstable —reads fail, writes still land, exactly what a stalled path does:
With reads still failing at
mkfstime the outcome is worse than a cleanreformat: the discard lands,
mkfsthen fails partway, and the device is leftwith no valid filesystem at all — an unmountable volume whose data is already
gone, presenting as a mount failure rather than as a reformat.
Both recipes are in
operator/docs/tests/test-plan-node-stage-format.md.The fix
Take the decision away from mount-utils.
atlas-lib/blockfsreads the first128 KiB of the device and reports what it found, keeping "read successfully,
found nothing" apart from "could not read":
formatAndMountmount-utils still performs the format for a device proven blank, so the mkfs
flags, the XFS feature pinning, and the stripe geometry are untouched. Only its
decision is replaced.
Details worth knowing:
is there whatever became of the rest. Zeros from a partial read never conclude
blank.
nvme_core.io_timeout, soa stalled path resolves as unreadable instead of hanging the stage. The read
runs on its own goroutine because a read against a dead path is not
interruptible.
Mount, matching whatthe restage path already does. ext4 and XFS each replay their own journal.
Refusing to stage is the deliberate trade: an outage is recoverable and a wiped
volume is not.
Relationship to #481
@noctarius's #481 addresses the same incident from the other end, and the two are
complementary rather than alternatives — its per-claim
on-disk-filesystemannotation is a real feature and a genuine second line of defense. They will
conflict textually in
nodeserver.go(both addctxtostageVolume, and bothmake the same house-style prose fixes); happy to rebase onto whichever lands
first.
One thing worth checking together, and the reason I did not simply add to #481:
its
getDiskFormatkeeps upstream'sexit.ExitCode() == 2 -> return "", nil, soan unreadable device still classifies as blank. #481 catches that case through
the claim annotation instead, which covers a volume this driver has staged since
the change but leaves two paths open — a volume with no annotation yet (anything
staged only by an older driver), and any stage where the claim cannot be read,
since
annotatedFilesystemtreats every error as "no annotation" and continuesto the format. Reading the device closes it without needing API access or RBAC.
Testing
Regression tests proven red first —
mkfs.ext4 -F -m0ran on both a deviceholding a filesystem and a device that could not be read — then green.
The format decision lives in mount-utils'
mount_linux.go, so these tests onlyexercise anything on Linux; on macOS
FormatAndMountSensitivemounts withoutprobing and they pass vacuously. Build and run them on Linux:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go test -c -o /tmp/spdk.test ./pkg/spdk/ /tmp/spdk.test -test.run TestStageVolume -test.vatlas-lib/blockfs, including EIO, timeout, cancellation, andpartial reads, under
-race.csi-driver/pkg/spdk, covering both refusals, the blankdevice that must still be formatted, the mismatched filesystem, and raw block.
pkg/spdkon Linux: 36 passed, 0 failed, CSI sanity suite included.csi-driverandatlas-libsuites green under-race;make lintclean inboth; all 9 house-style gates pass.
Live, on a real volume
Against a real simplyblock ext4 volume on a four-node K3s cluster, with
blkidreporting nothing (
TestLive*, env-gated and skipped by default):The fixed path issued no commands at all — it read the device, saw ext4, and
mounted it. A freshly provisioned lvol still reads as blank, so first-time
formatting is unaffected.
Not yet deployed end-to-end. Everything live ran as a cross-compiled test
binary; no cluster runs this image yet.
Upstream
kubernetes/kubernetes#140376
— same defect, reached through an
fsckthat corrupted the primary superblock,open and untriaged since 2026-07-09 with no fix PR. Neither approach proposed
there covers a device that will not answer a read, so this is ours to fix.
mount-utilsitself has issues disabled; it is a publishing mirror.🤖 Generated with Claude Code