Repository navigation
feat(dashboard): report per-drive filesystem free space - #1041
Conversation
Scrutiny collected filesystem capacity per host, but nothing connected a filesystem to the drive underneath it, so neither the dashboard card nor the device detail page could report free space for a drive. The filesystem collector now walks the kernel holder tree under /sys/class/block from each mount source down to the whole disks beneath it, and uploads those disk names with the snapshot. Partitions, LVM, md and LUKS all resolve through the same walk, which is why the walk reads sysfs rather than parsing the source device path: trimming digits off a partition name is wrong for every device whose name already ends in one, and a device-mapper path names no disk at all. The links are stored one row per (filesystem, disk) pair in a new filesystem_devices table, because the relation is many-to-many: a disk carries several filesystems, and an LVM or md filesystem spans several disks. Both the device summary and the device detail response carry the filesystems a drive backs, and the detail response also carries the collector status for the drive's host. A single free-space figure is reported for a drive only when exactly one filesystem sits on it and that filesystem sits on no other drive. Every other case shows a mount count on the card and a per-filesystem breakdown on the detail page; shared free space is never divided between drives, because the kernel reports no such split. A filesystem the collector could not attribute stays attributed to nothing, and the detail page explains which of the three possible reasons applies rather than reporting zero bytes free. Closes #470
Review of #1041 found two ways the per-drive figures could state something false, and four smaller problems. An md array lists every member under slaves, including a hot spare, so the array's filesystem was attributed to a disk holding none of its data. The walk now reads each member's role from md/dev-<name>/state and skips a spare. A member whose state cannot be read is kept: over-attributing one drive is a smaller error than hiding a data-carrying drive's filesystems. A dm-cache or bcache cache device is still attributed the filesystem it fronts, because the holder tree alone does not record which role a member plays; that limit is now documented. A block device holds one filesystem, so two mounts reporting the same source device are one filesystem seen twice. A bind mount carries the underlying device as its source, and btrfs subvolume mounts repeat it, so the detail panel listed one filesystem's free space several times over and the card fell back to a mount count for a drive with a single filesystem on it. Per-drive reads now collapse repeated mounts of one source. The per-host capacity panel keeps one row per mount, which is the point there, so this is applied on the per-drive paths only. Only sources naming a block device are collapsed: distinct mounts may legitimately share a source string such as "none". Also from the review: - The Filesystems panel is rendered only once a host status exists, so a deployment that never enabled the filesystem collector no longer gains an empty card on every drive. - The empty-list message named LVM, mdadm and LUKS but not a collector older than the backing-device field, which the rollout notes call the expected cause during a staged upgrade. - docs/FILESYSTEM_CAPACITY.md claimed the device cards report free space, without the qualification that they do so only when attribution is unambiguous. - The openapi document is 3.0.3, where sibling keys beside $ref are ignored, so filesystem_status lost its description. It is wrapped in allOf now. - Moving the filesystem helpers below hasSelfTestSupport() returns that method's doc comment to it.
CI triage at head
|
| Package | Result |
|---|---|
collector/pkg/blockdev |
ok, 89.4% coverage |
collector/pkg/filesystem |
ok |
webapp/backend/pkg/database |
ok, 59.2% coverage |
Test Frontend, Lint Backend (Go), and all ten cross-platform builds are green, so the frontend build and Angular specs are verified by CI.
This stays in draft until #1040 merges and a develop merge here turns Test Backend green. The remaining gate after that is an independent review against the then-current head.
…ead paths The remaining review findings on #1041. Purging a SMART host left its filesystem capacity, host status and device link rows behind, where they outlive every device they could describe. Whichever drive next registered under the same device name on the same host id inherited the purged host's free space. All three are now deleted inside the same transaction as the host's devices, and the host-management test fixture migrates those tables because they are part of a host's data. Loop, zram and ram devices are no longer reported as backing devices. They are kernel-synthesized rather than drives, so Scrutiny never registers one and a link naming it could match nothing; a snap-based host was writing one such row per snap package. The read paths had no tests. Added coverage for the device details handler (only this drive's filesystems, no leakage from another host's drive of the same name, repeated mounts of one filesystem collapsed, the status key omitted rather than null when a host never reported, and SMART data still served when the filesystem read fails) and for attachFilesystems (per-drive attribution, host isolation, a RAID passthrough device name matching nothing, and summaries left intact when the table is unreadable). The detail component's filesystem helpers are covered too. Also from the review: - The empty-list message now names a host id that differs between the filesystem and SMART collectors, which otherwise causes silent total non-attribution. - The usage bar carries progressbar semantics instead of being an unlabelled div. - The filesystems test file scopes its in-memory SQLite DSN by test name, matching the other test files in the package, so a later test using a second host id cannot break a neighbour. Not taken: the collector's default empty host.id lets two hosts share an identity, which cross-attributes filesystems between them. That predates this work, already collides on the host capacity panel, and changing host identity belongs in its own change.
Gating the Filesystems panel on a host status existing stopped a deployment that never enabled the filesystem collector from gaining an empty card on every drive, but it also silenced the case the panel most needs to report. The filesystem collector maps an empty host.id to "default" while the SMART collector stores it unchanged, so an omnibus deployment that enables the collector without setting COLLECTOR_HOST_ID attributes nothing: devices carry one host id and filesystems another. No host status exists for the device's host id, so the gate was false, so the message written for exactly that mismatch never rendered. The feature was inert and unexplained on the default configuration. The device details response now carries filesystem_collector_reporting, true when the collector has reported for any host. That is what separates a collector running under another host id from a collector that was never enabled, which a per-host status cannot distinguish. The panel renders when it is true and names the mismatch, quoting the drive's host id; with nothing reported anywhere the panel still does not render, because there is nothing drive-specific to say. The underlying identity collision stays in #1046, which is where changing host identity belongs. This only stops the UI from going quiet about it.
…-filesystem-capacity # Conflicts: # webapp/backend/pkg/web/handler/get_device_details.go
Summary
Scrutiny collected filesystem capacity per host, but nothing connected a filesystem to the drive underneath it, so neither the dashboard card nor the device detail page could report free space for a drive. This adds that link and surfaces it on both surfaces, which closes the remaining gap in #470 (self-test history and performance benchmarks already shipped).
Type of Change
Related Issues
Closes #470
Filed separately during review, deliberately not fixed here: #1046 (collectors share an empty default
host.id, so unconfigured hosts are treated as one). That collision predates this work and changing host identity needs its own migration story.Changes Made
Resolving a mount to its drives
collector/pkg/blockdevwalks the kernel holder tree under/sys/class/blockfrom each mount source down to the whole disks beneath it. Partitions, LVM, md and LUKS resolve through the same walk. The walk reads sysfs rather than parsing the source device path on purpose: trimming digits off a partition name is wrong for every device whose name already ends in one (nvme0n1p2,md1p1,mmcblk0p1), and a device-mapper path such as/dev/mapper/vg-rootnames no disk at all.md/dev-<name>/stateand a spare is skipped. A member whose state cannot be read is kept: over-attributing one drive is a smaller error than hiding a data-carrying drive's filesystems.backing_devicesonFilesystemCapacity.Storing and serving the link
filesystem_devicestable (migrationm20261027000000), one row per (filesystem, disk) pair, because the relation is many-to-many: a disk carries several filesystems, and an LVM or md filesystem spans several disks. Links are replaced with the snapshot that owns them, so a moved mount leaves no stale attribution.GET /api/device/:id/detailscarriesfilesystemsandfilesystem_status; the device summary used byGET /api/summarycarriesfilesystemsper device.none. The per-host capacity panel still lists every mount, which is the point there.UI
progressbarsemantics, and a "shared with" note naming the other drives when a filesystem spans more than one. The panel renders only once a host status exists, so a deployment that never enabled the filesystem collector gains no empty card on every drive.filesystem_collector_reporting, true when the filesystem collector has reported for any host. A per-host status cannot distinguish a collector running under another host id from one that was never enabled, and the two need different answers: the first gets the mismatch named, quoting the drive's host id, and the second gets no panel at all. This matters on the default configuration, where the filesystem collector maps an emptyhost.idtodefaultand the SMART collector stores it unchanged, so nothing attributes untilCOLLECTOR_HOST_IDis set for both. The identity collision itself is [BUG] Collectors share an empty default host.id, so unconfigured hosts are treated as one #1046.filesystemUsageClassmoved toapp/shared/filesystem-usage.utils.tsso the dashboard panel and the new detail panel cannot disagree about a filesystem's colour.Docs
docs/FILESYSTEM_CAPACITY.mdrecords the attribution model and its limits: Linux-only, md spares skipped, dm-cache and bcache cache devices still attributed the filesystem they front, btrfs multi-device attributed to one disk, repeated mounts collapsed per drive, and filesystem rows purged with their host.docs/openapi.yamldocuments the new fields.filesystem_statususesallOfbecause this document is 3.0.3, where sibling keys beside$refare ignored.Testing
On this head CI has
Test Frontend,Lint Backend (Go),Test Backendand every Linux, Windows and FreeBSD build green. See the CI note at the end of this body for the two checks that are not.Locally:
go build ./...,go vet ./webapp/backend/... ./collector/..., andgolangci-lint runover every changed Go package are clean, with only findings that predate this branch remaining.New tests:
collector/pkg/blockdev/backing_test.gobuilds a fake sysfs and/devtree (two disks with partitions, an md array across both, an array with a hot spare, an LVM volume, an nvme namespace, a loop device, a mapper symlink) and covers whole disks, partitions, stacked devices, spare skipping, synthetic disks, non-block sources, unknown devices, a missing sysfs root and a cyclic holder tree.collector/pkg/filesystem/detect_test.goasserts backing devices reach the snapshot and that a network mount is still reported with no attribution.scrutiny_repository_filesystems_test.gocovers grouping by drive, duplicate backing devices against the primary key, stale-link removal, source dedupe, andattachFilesystems(per-drive attribution, host isolation, a RAID passthrough device name matching nothing, and summaries left intact when the table is unreadable).scrutiny_repository_hosts_test.goasserts a purged host's filesystem rows are gone and an unselected host's survive.get_device_details_test.gocovers the handler: only this drive's filesystems, no leakage from another host's drive of the same name, repeated mounts collapsed,filesystem_statusomitted rather than null when a host never reported, and SMART data still served when the filesystem read fails.dashboard-device.component.spec.tscovers the four card cases;detail.component.spec.tscovers the panel's helpers.Each fix was neutered to confirm its test goes red: the partition-to-disk step, the spare filter, the stale-link delete, the source dedupe, and the purge cleanup.
Checklist
Additional Notes
Review: required (adds a database migration)
Rollout notes:
filesystem_devicesand touches no existing table.COLLECTOR_FILESYSTEM_CRON_SCHEDULE,COLLECTOR_FILESYSTEM_RUN_STARTUP), and this PR does not change that default.CI notes, neither caused by this change:
npm auditunder Dependency Audit fails on fiveundiciadvisories inside npm's own bundled dependency tree, against a three-entry allowlist in.github/scripts/audit-release-tooling.mjs, which this PR does not touch. It currently fails on other open branches too, but the cause is advisory timing rather than anything branch-specific:develop's last Dependency Audit run passed on 2026-10-25, before these advisories were published.Build darwin/amd64andBuild darwin/arm64were cancelled on an earlier head after 15 minutes with no steps recorded, amacos-latestrunner that never picked the job up rather than a build failure. Re-running them passed.TestIsMountPointSuitable_RequiresHeadroomAboveTestFileincollector/pkg/performancefailed on earlier heads of this branch, being environment-dependent on whether the runner's temp directory is tmpfs. fix(performance): cover the benchmark headroom rule without a filesystem #1040 has since merged and this branch now carries it, so the flake is gone. This PR touches neither that package nor itscollector/pkg/mountsdependency.develophas no required status checks, so none of these gate the merge button; they are recorded so a reviewer does not re-chase them.