Files
FabledSteward/tests/plugins/host_agent/test_routes_expand_metrics.py
T
bvandeusenandClaude Opus 5.5 5dd9956f35
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m19s
CI / unit (push) Successful in 1m27s
CI / publish (push) Successful in 1m13s
feat(host_agent): a disk out of inodes reads as full (#5745)
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>
2026-10-14 16:51:46 -04:00

114 lines
4.1 KiB
Python

"""Pure-function tests for host_agent metric expansion."""
from datetime import datetime, timezone
from plugins.host_agent.routes import _expand_sample_to_metrics
NOW = datetime(2026, 4, 14, 12, 0, 0, tzinfo=timezone.utc)
def _sample() -> dict:
return {
"ts": NOW.isoformat(),
"cpu_pct": 12.5,
"mem": {
"total_bytes": 16_000_000_000,
"used_bytes": 8_000_000_000,
"available_bytes": 8_000_000_000,
"swap_used_bytes": 0,
},
"load": {"1m": 0.1, "5m": 0.2, "15m": 0.3},
"uptime_secs": 1234,
"storage": [
{"mount": "/", "total_bytes": 1000, "used_bytes": 400},
{"mount": "/mnt/data", "total_bytes": 2000, "used_bytes": 1800},
],
}
def _by_metric(rows, name):
return [r for r in rows if r.metric_name == name]
def test_expand_emits_cpu_mem_load_uptime():
rows = _expand_sample_to_metrics(_sample(), "testhost", NOW)
names = {r.metric_name for r in rows}
assert "cpu_pct" in names
assert "mem_used_pct" in names
assert "mem_available_bytes" in names
assert "swap_used_bytes" in names
assert "load_1m" in names and "load_5m" in names and "load_15m" in names
assert "uptime_secs" in names
def test_expand_source_module_and_recorded_at():
rows = _expand_sample_to_metrics(_sample(), "testhost", NOW)
assert rows, "expected non-empty rows"
for r in rows:
assert r.source_module == "host_agent"
assert r.recorded_at == NOW
def test_expand_cpu_value_and_resource():
cpu = _by_metric(_expand_sample_to_metrics(_sample(), "testhost", NOW), "cpu_pct")
assert len(cpu) == 1
assert cpu[0].value == 12.5
assert cpu[0].resource_name == "testhost"
def test_expand_mem_used_pct_math():
# used = total - available = 50%
used = _by_metric(_expand_sample_to_metrics(_sample(), "testhost", NOW), "mem_used_pct")
assert len(used) == 1
assert abs(used[0].value - 50.0) < 0.01
def test_expand_per_mount_disk_metrics():
rows = _expand_sample_to_metrics(_sample(), "testhost", NOW)
per_mount = _by_metric(rows, "disk_used_pct")
assert {r.resource_name for r in per_mount} == {"testhost:/", "testhost:/mnt/data"}
# Values: / is 40%, /mnt/data is 90%
by_res = {r.resource_name: r.value for r in per_mount}
assert abs(by_res["testhost:/"] - 40.0) < 0.01
assert abs(by_res["testhost:/mnt/data"] - 90.0) < 0.01
def test_expand_disk_worst_is_max():
worst = _by_metric(_expand_sample_to_metrics(_sample(), "testhost", NOW), "disk_used_pct_worst")
assert len(worst) == 1
assert worst[0].resource_name == "testhost"
assert abs(worst[0].value - 90.0) < 0.01
def test_expand_skips_missing_cpu_and_load():
sample = {"mem": {"total_bytes": 100, "available_bytes": 50}}
rows = _expand_sample_to_metrics(sample, "h", NOW)
names = {r.metric_name for r in rows}
assert "cpu_pct" not in names
assert "load_1m" not in names
assert "uptime_secs" not in names
assert "mem_used_pct" in names
def test_expand_empty_storage_emits_no_disk_worst():
sample = {"cpu_pct": 1.0, "storage": []}
rows = _expand_sample_to_metrics(sample, "h", NOW)
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