Case study · Storage
The 33-hour wipe: a CSI driver reformatted a live LUN
A routine node drain ended with 33 hours of production telemetry gone. A
democratic-csi
driver ran mkfs
over a live Pure LUN, triggered by a dm-multipath path-group race. Here is the incident,
and the exact emisar loop that contained it: investigate through declared actions, stop the
bleed behind one approval, then write the durable fix to the infra repo, with no raw
SSH and every step on the record. The one-line setting
you'd reach for first was a no-op. What stopped it was a guard that
refuses to trust the driver.
The stack
Five Dell PowerEdge R640s in the VA1 colo · Pure FlashArray over iSCSI multipath · Nomad
2.0.2 · democratic-csi node-manual
v1.9.5, pinned by OCI digest. VictoriaMetrics, VictoriaLogs, and Grafana each own a
dedicated Pure LUN (vm-data, vl-data, grafana-data).
All three are single-node-writer, all sit under Pure QoS caps, and all hold 33
hours of Apex / Fortnite / Tarkov / Deadlock backend telemetry. The investigation below runs
entirely
through emisar on the stock debugging, docker, nomad, multipath, iscsi,
pure
and victoriametrics
packs. emisar is only as capable as the actions you
declare; for this fleet, the catalog already covered every layer of the fabric.
The fabric was never quiet
The wipe had warning signs. For the weeks before it, the iSCSI / dm-multipath
path was a slow drip of operational pain: queue depth tuned to 128 for the FlashArray,
multipath retry behavior corrected, persistent iSCSI sessions pinned at boot, an iSCSI
login race plus a /run/multipathd
persistence bug, a data-plane boot race, a session watchdog timer, and
kill_timeout
raised to 60s so NodeUnstageVolume
could flush on drain. None of these was catastrophic alone. Together they made an unstable
base where a path group could come up with no active path. That is exactly the
condition that makes a live device read empty.
T+0: the drain
The maintenance was routine: drain nomad-hvn01, reboot, restore. VictoriaMetrics
and VictoriaLogs rescheduled onto nomad-hvn03
and came up green in about 30 seconds. Ingest resumed, and everything looked dull and normal.
T+50m: 33 hours gone
Grafana over the last 7 days showed almost everything flat, with only the last 12 minutes of metrics alive and a sharp cliff where the history just ended. Logs looked the same. The processes were healthy and ingesting; the history was missing because the data itself was not there. An agent picks up the page.
1 · Investigate through emisar, not SSH
The agent works down the layers with declared pack actions, each scoped to
nomad-hvn03
(or the array) and logged with a reason. It has no shell and no standing SSH key.
# Claude, over MCP → emisar. Each call is a declared, scoped, logged action. nomad.csi_volume_status {"volume_id": "vm-data"} → Schedulable = true Access Mode = single-node-writer Allocations a1b2c3d4 vm running (nomad-hvn03) # the storage layer is fine — the volume is attached and healthy. # so the question is what's actually on it. fs.ls_long {"path": "…/vm-data"} → total 24 drwx------ 2 root root 16384 13:11 lost+found # a freshly-made ext4: nothing but lost+found, born at 13:11. # 33 hours of VictoriaMetrics data is not here. debugging.dmesg_tail {"lines": 400} → 13:11:03 device-mapper: multipath 254:3: queue_if_no_path enabled 13:11:03 multipath 254:3: Reinstating path … remaining active paths: 0 13:11:09 EXT4-fs (dm-3): mounted filesystem … clean # six seconds with zero active paths, then a clean mount of a fresh fs. multipath.topology → 3624a9…265c dm-3 PURE,FlashArray features='1 queue_if_no_path' hwhandler='1 alua' wp=rw |-+- policy='service-time 0' prio=0 status=enabled # ← neither path | `- 8:0:0:1 sde 8:64 active ready running # group is `-+- policy='service-time 0' prio=0 status=enabled # status=active: `- 10:0:0:1 sdi 8:128 active ready running # the I/O window docker.logs {"container": "democratic-csi", "lines": 2000} → GetDiskFormat /dev/mapper/3624a9…265c → blkid output="" (empty) Disk appears unformatted; running mkfs.ext4 -F /dev/mapper/3624a9… Disk successfully formatted (mkfs) # blkid read empty mid-race; the driver formatted a live LUN. pure.volumes_space {"names": "vm-data"} → vm-data data_reduction 1.0:1 unique 0.01G (40.8G at 13:00) # the array agrees: unique data fell off a cliff. Gone, not hidden.
Diagnosis: when the multipath device was re-probed on migration, every path came up
enabled
(queue-ready) but no group was promoted active. blkid
was the first I/O: it opened the device, the read sat in the queue, and it timed out
empty. The driver read "empty" as "unformatted" and ran mkfs.ext4 -F
over a live LUN, then mounted the fresh filesystem for VictoriaMetrics to write to.
The failure belongs to a whole bug class, tracked in
kubernetes/kubernetes#95183
and confirmed against NetApp Trident, Longhorn, OpenEBS, and Azure Disk.
Switching CSI drivers would not fix it, because the same bug lives in all of them.
2 · Stop the bleed with one approval
VictoriaMetrics is already writing fresh parts to the empty filesystem, and every
group-commit overwrites blocks that still hold the old data. The move is to halt it, and
nomad.alloc_stop
is declared risk: high
in the pack, so policy holds it for a human.
nomad.alloc_stop {"alloc_id": "a1b2c3d4", "reason":
"CSI reformatted a live LUN — stop writes to preserve recoverable blocks"}
⏸ pending approval — nomad.alloc_stop is risk:high; a human approves in the portal
✓ approved by you · one use · audit event recorded
→ alloc stopped · writes halted · LUN frozen for forensics
Caught in the first minute, that freeze preserves the LUN for recovery. Here a human took an hour to notice, the ext4 journal had wrapped, and the old blocks were already reused. So we accepted the 33-hour gap (game-side state was untouched; only telemetry was lost).
3 · Codify the fix that actually stops it
The obvious fix is one line in the driver config, and it does nothing. Source review
during the cutover showed democratic-csi v1.9.5 never reads
node.format.disabled
on the POSIX NodeStageVolume
path; it is a documented no-op. The fix that holds is a guard that doesn't trust
the driver. It landed in the infra repo as a reviewed pull request, written locally for a
human to merge.
# driver-config.yaml — the obvious knob, kept only as documentation: node: { format: { disabled: true } } # ← v1.9.5 never reads it. No-op. # So don't let the driver reach a real mkfs. At container start, shadow every # formatter and keep the real binary as <name>.real: for name in mkfs mkfs.ext2 mkfs.ext3 mkfs.ext4 mkfs.xfs mkfs.btrfs; do for dir in /usr/sbin /sbin /usr/bin /bin; do [ -x "$dir/$name" ] || continue mv "$dir/$name" "$dir/$name.real" cp /local/mkfs.guard "$dir/$name" done done # mkfs.guard — runs in the driver's place and decides per device: tool=$(basename "$0"); real=$(command -v "$tool.real") for arg in "$@"; do case "$arg" in /dev/*|/host/dev/*) ;; *) continue ;; esac base=$(basename "$(readlink -f "$arg")") id="$base $(cat /sys/class/block/$base/device/model 2>/dev/null)" echo "$id" | grep -Eqi 'nvme|Pure|FlashArray' || continue # local disk: allow [ "${ALLOW_PURE_MKFS_DEVICE:-}" = "$arg" ] && continue # explicit one-off if [ "$tool" = mkfs.ext4 ]; then fstype=$(blkid -p -s TYPE -o value "$arg"); rc=$? case $rc in 0) [ "$fstype" = ext4 ] && exit 0 ;; # already ext4: idempotent no-op 2) ;; # blank, but still not ours to format *) exit 64 ;; # blkid ambiguous: FAIL CLOSED esac fi exit 64 # any Pure/NVMe LUN we didn't no-op above: refuse, loudly done exec "$real" "$@" # not a Pure device: the real mkfs runs # Same era, for other reasons: iSCSI dm-multipath → NVMe/TCP. Rarer # empty-read window — but not the fix; the driver reformatted on NVMe too.
The guard shadows every mkfs
entrypoint inside the plugin and fails loud on anything it can't prove is a blank device.
Even a corrupted filesystem that looks empty to blkid
is refused. A separate serial-resolved formatter handles genuinely new
volumes: it resolves exactly one Pure namespace by serial, then refuses unless the start,
middle, and end of the device all read as zero.
What emisar changed
-
The forensics were readable and scoped.
Every
dmesg, multipath, CSI-log, and array-side read was a declared action against one host, logged with a reason, instead of atailscale ssh root@…scramble across five tools with no record of who looked at what. - The one destructive step stopped for a person. Halting the alloc was gated, approved once, and recorded, so the agent could contain the damage without being trusted to run arbitrary commands.
- The real fix landed as reviewable infra. The guard that distrusts the driver landed as a diff a human reviewed and merged, instead of a command that lived for ten minutes in someone's shell history and got lost.
emisar would not have stopped democratic-csi's mkfs.
That was an automated component doing its job badly, and the tidy declarative fix you'd
reach for first was a no-op the vendor shipped. What emisar changes is everything a human
or agent does around
the failure: investigating through tools, stopping the disaster where you can, and handing
back a change a human reviews and merges.