feat(host_agent): a disk out of inodes reads as full (#5745)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m19s
CI / unit (push) Successful in 1m27s
CI / publish (push) Successful in 1m13s

VDNT-Util01's / ran out of inodes (956 free of 60.8M) with 274G of space
left; every file create failed and Steward's page read healthy at 66%.

- Agent 1.13.0 reports each mount's inodes (statvfs; a filesystem reporting
  none, like btrfs, is left without).
- Ingest records inodes_used_pct per mount, and disk_used_pct_worst is the
  fullest mount by space or inodes, so existing disk-full rules and the
  overview see it.
- The host's health strip, the Hosts widget and the host glance grade / by
  whichever is fuller and name inodes when they are ("Disk / 66% · inodes
  100%", "Inodes on / ... past its critical threshold").

Hosts pick up the inode figures once their agent is updated to 1.13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 16:51:46 -04:00
co-authored by Claude Opus 5.5
parent 1b82d8ad83
commit 5dd9956f35
11 changed files with 85 additions and 16 deletions
@@ -78,10 +78,19 @@ def test_collect_storage():
r.free = free
return r
with patch.object(a.shutil, "disk_usage", side_effect=_fake_usage):
# Inodes (#5745): "/" has 100 with 1 free; /mnt/data (btrfs-like) reports none.
inodes = {"/": (100, 1), "/mnt/data": (0, 0)}
def _fake_statvfs(path):
files, ffree = inodes[path]
return type("S", (), {"f_files": files, "f_ffree": ffree})()
with patch.object(a.shutil, "disk_usage", side_effect=_fake_usage), \
patch.object(a.os, "statvfs", side_effect=_fake_statvfs):
out = a.collect_storage(["/", "/mnt/data"])
assert out == [
{"mount": "/", "total_bytes": 1000, "used_bytes": 400},
{"mount": "/", "total_bytes": 1000, "used_bytes": 400,
"inodes_total": 100, "inodes_used": 99},
{"mount": "/mnt/data", "total_bytes": 2000, "used_bytes": 500},
]
@@ -96,3 +96,18 @@ def test_expand_empty_storage_emits_no_disk_worst():
names = {r.metric_name for r in rows}
assert "disk_used_pct_worst" not in names
assert "cpu_pct" in names
def test_a_mount_out_of_inodes_is_the_fullest_disk():
"""#5745: VDNT-Util01's / was 66% full by space and 100% by inodes, every
file create failed, and Steward read it healthy. Inode use is recorded per
mount, and the fullest disk is the fullest by either."""
sample = _sample()
sample["storage"][0].update(inodes_total=60_850_176, inodes_used=60_849_220)
rows = _expand_sample_to_metrics(sample, "testhost", NOW)
(inodes,) = _by_metric(rows, "inodes_used_pct")
assert inodes.resource_name == "testhost:/" and inodes.value > 99.99
(worst,) = _by_metric(rows, "disk_used_pct_worst")
assert worst.value == inodes.value
# A filesystem reporting no inodes (btrfs) records none.
assert len(_by_metric(_expand_sample_to_metrics(_sample(), "testhost", NOW), "inodes_used_pct")) == 0