Docker02's outbound traffic tops out the internet connection and Steward couldn't say who sends it: a container's counters were kept only as a running total, and NFS traffic belongs to no container. - Docker ingest turns each running container's cumulative network counters into net_rx_bps / net_tx_bps on "<host>/<container>" (net_rates), counting from the stored row so a restart of Steward loses nothing; a container new, recreated or restarted has no rate until its next snapshot. - Agent 1.25.0 reads the bytes sent and received per NFS mount from mountstats (already parsed for the delays) as tx_bps / rx_bps; Steward stores them as nfs_tx_bps / nfs_rx_bps on "<host>:<mount>". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
8.0 KiB
Python
208 lines
8.0 KiB
Python
# tests/plugins/host_agent/test_agent_collectors.py
|
|
"""Unit tests for per-resource collectors, driven by fixture /proc files."""
|
|
from unittest.mock import patch
|
|
import pytest
|
|
from plugins.host_agent import agent as a
|
|
|
|
|
|
PROC_STAT_SAMPLE_1 = "cpu 100 0 50 850 0 0 0 0 0 0\n"
|
|
PROC_STAT_SAMPLE_2 = "cpu 200 0 100 1700 0 0 0 0 0 0\n"
|
|
|
|
PROC_MEMINFO = (
|
|
"MemTotal: 16000000 kB\n"
|
|
"MemFree: 2000000 kB\n"
|
|
"MemAvailable: 6000000 kB\n"
|
|
"SwapTotal: 4000000 kB\n"
|
|
"SwapFree: 3500000 kB\n"
|
|
)
|
|
|
|
PROC_LOADAVG = "0.42 0.55 0.61 1/234 5678\n"
|
|
PROC_UPTIME = "482934.12 1000000.00\n"
|
|
OS_RELEASE = 'PRETTY_NAME="Ubuntu 24.04 LTS"\nNAME="Ubuntu"\n'
|
|
|
|
|
|
def test_collect_memory_uses_available(tmp_path):
|
|
p = tmp_path / "meminfo"
|
|
p.write_text(PROC_MEMINFO)
|
|
with patch.object(a, "MEMINFO_PATH", str(p)):
|
|
mem = a.collect_memory()
|
|
assert mem["total_bytes"] == 16000000 * 1024
|
|
assert mem["available_bytes"] == 6000000 * 1024
|
|
assert mem["used_bytes"] == (16000000 - 6000000) * 1024
|
|
assert mem["swap_used_bytes"] == (4000000 - 3500000) * 1024
|
|
|
|
|
|
def test_collect_load(tmp_path):
|
|
p = tmp_path / "loadavg"
|
|
p.write_text(PROC_LOADAVG)
|
|
with patch.object(a, "LOADAVG_PATH", str(p)):
|
|
load = a.collect_load()
|
|
assert load == {"1m": 0.42, "5m": 0.55, "15m": 0.61}
|
|
|
|
|
|
def test_collect_uptime(tmp_path):
|
|
p = tmp_path / "uptime"
|
|
p.write_text(PROC_UPTIME)
|
|
with patch.object(a, "UPTIME_PATH", str(p)):
|
|
assert a.collect_uptime() == 482934
|
|
|
|
|
|
def test_collect_cpu_reads_twice():
|
|
# Two sequential _read_file calls return two stat snapshots.
|
|
reads = iter([PROC_STAT_SAMPLE_1, PROC_STAT_SAMPLE_2])
|
|
|
|
def _read(_path):
|
|
return next(reads)
|
|
|
|
with patch.object(a, "_read_file", _read), \
|
|
patch.object(a.time, "sleep", lambda _s: None):
|
|
pct, cores = a.collect_cpu(sample_window=0.0)
|
|
# totals: 1000 → 2000 (delta 1000). idle+iowait: 850 → 1700 (delta 850).
|
|
# busy = 150. pct = 15.0. No cpuN lines in the fixture → no per-core values.
|
|
assert pct == pytest.approx(15.0, abs=0.1)
|
|
assert cores == []
|
|
|
|
|
|
def test_collect_storage():
|
|
fake = {"/": (1000, 400, 600), "/mnt/data": (2000, 500, 1500)}
|
|
|
|
def _fake_usage(path):
|
|
total, used, free = fake[path]
|
|
|
|
class R:
|
|
pass
|
|
|
|
r = R()
|
|
r.total = total
|
|
r.used = used
|
|
r.free = free
|
|
return r
|
|
|
|
# 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,
|
|
"inodes_total": 100, "inodes_used": 99},
|
|
{"mount": "/mnt/data", "total_bytes": 2000, "used_bytes": 500},
|
|
]
|
|
|
|
|
|
def test_collect_metadata(tmp_path):
|
|
p = tmp_path / "os-release"
|
|
p.write_text(OS_RELEASE)
|
|
|
|
class Uname:
|
|
release = "6.8.0-45-generic"
|
|
machine = "x86_64"
|
|
|
|
with patch.object(a, "OS_RELEASE_PATH", str(p)), \
|
|
patch.object(a.os, "uname", return_value=Uname()):
|
|
meta = a.collect_metadata()
|
|
assert meta["kernel"] == "6.8.0-45-generic"
|
|
assert meta["arch"] == "x86_64"
|
|
assert "Ubuntu 24.04" in meta["distro"]
|
|
|
|
|
|
def test_a_mount_that_does_not_answer_is_reported_stalled_and_not_probed_again():
|
|
"""#5760: statvfs on a stalled NFS share blocks with no timeout, and read
|
|
inline that froze the whole agent. The sample goes on without it, and a
|
|
probe still blocked isn't stacked on by the next sample."""
|
|
import threading
|
|
release = threading.Event()
|
|
calls: list[str] = []
|
|
|
|
def _usage(path):
|
|
calls.append(path)
|
|
if path == "/nfs/media":
|
|
release.wait(5)
|
|
return type("R", (), {"total": 1000, "used": 400, "free": 600})()
|
|
|
|
a._stuck_mounts.clear()
|
|
try:
|
|
with patch.object(a.shutil, "disk_usage", side_effect=_usage), \
|
|
patch.object(a.os, "statvfs", side_effect=OSError):
|
|
first = a.collect_storage(["/", "/nfs/media"], deadline=0.2)
|
|
second = a.collect_storage(["/", "/nfs/media"], deadline=0.2)
|
|
assert first == second == [
|
|
{"mount": "/", "total_bytes": 1000, "used_bytes": 400},
|
|
{"mount": "/nfs/media", "stalled": True},
|
|
]
|
|
assert calls.count("/nfs/media") == 1
|
|
finally:
|
|
release.set()
|
|
a._stuck_mounts.clear()
|
|
|
|
|
|
def test_a_mount_that_errors_is_left_out_not_the_whole_list():
|
|
def _usage(path):
|
|
if path == "/nfs/gone":
|
|
raise OSError(116, "Stale file handle")
|
|
return type("R", (), {"total": 10, "used": 1, "free": 9})()
|
|
|
|
with patch.object(a.shutil, "disk_usage", side_effect=_usage), \
|
|
patch.object(a.os, "statvfs", side_effect=OSError):
|
|
out = a.collect_storage(["/", "/nfs/gone"])
|
|
assert out == [{"mount": "/", "total_bytes": 10, "used_bytes": 1}]
|
|
|
|
|
|
# ─── NFS delay per mount (#5996) ─────────────────────────────────────────────
|
|
|
|
MOUNTSTATS = (
|
|
"device proc mounted on /proc with fstype proc\n"
|
|
"device 192.168.0.7:/volume1/data mounted on /nfs/data with fstype nfs4 statvers=1.1\n"
|
|
"\topts:\trw,vers=4.1,hard,proto=tcp\n"
|
|
"\tper-op statistics\n"
|
|
"\t NULL: 0 0 0 0 0 0 0 0 0\n"
|
|
"\t READ: 10 10 0 1200 40960 5 30 40 0\n"
|
|
"\t WRITE: 4 4 0 8192 512 2 20 24 0\n"
|
|
"\n"
|
|
"device 192.168.0.5:/volume1/My Media mounted on /nfs/my\\040media with fstype nfs statvers=1.1\n"
|
|
"\tper-op statistics\n"
|
|
"\t GETATTR: 3 3 0 300 300 0 3 3 0\n"
|
|
"device /dev/sda1 mounted on / with fstype ext4\n"
|
|
)
|
|
|
|
|
|
def test_mountstats_reads_each_nfs_mount_s_ops_and_skips_the_rest():
|
|
got = a.read_mountstats(MOUNTSTATS)
|
|
assert set(got) == {"/nfs/data", "/nfs/my media"}
|
|
assert got["/nfs/data"]["READ"] == (10, 10, 0, 5, 30, 40, 1200, 40960)
|
|
assert got["/nfs/data"]["NULL"] == (0, 0, 0, 0, 0, 0, 0, 0)
|
|
assert got["/nfs/my media"] == {"GETATTR": (3, 3, 0, 0, 3, 3, 300, 300)}
|
|
|
|
|
|
def test_mountstats_that_cannot_be_read_is_no_mounts(tmp_path):
|
|
with patch.object(a, "MOUNTSTATS_PATH", str(tmp_path / "missing")):
|
|
assert a.collect_nfs_raw() == {}
|
|
|
|
|
|
def test_nfs_delay_is_what_the_requests_since_the_last_sample_took():
|
|
prev = {"/nfs/data": {"READ": (10, 10, 0, 5, 30, 40, 1200, 40960),
|
|
"WRITE": (4, 4, 0, 2, 20, 24, 8192, 512)},
|
|
"/nfs/idle": {"GETATTR": (3, 3, 0, 0, 3, 3, 300, 300)},
|
|
"/nfs/remounted": {"READ": (50, 50, 0, 0, 50, 50, 5000, 5000)}}
|
|
cur = {"/nfs/data": {"READ": (20, 21, 1, 7, 130, 240, 21680, 450560),
|
|
"WRITE": (4, 4, 0, 2, 20, 24, 8192, 512)},
|
|
"/nfs/idle": {"GETATTR": (3, 3, 0, 0, 3, 3, 300, 300)},
|
|
"/nfs/remounted": {"READ": (2, 2, 0, 0, 2, 2, 200, 200)},
|
|
"/nfs/new": {"READ": (5, 5, 0, 0, 5, 5, 500, 500)}}
|
|
got = {r["mount"]: r for r in a.nfs_delays(cur, prev, 10.0)}
|
|
# ten READs in 10 s, 100 ms of round trip and 200 ms of waiting between
|
|
# them, one sent twice and one that hit a major timeout; no WRITE finished.
|
|
# 20,480 bytes sent and 409,600 received on the wire.
|
|
assert got["/nfs/data"] == {"mount": "/nfs/data", "ops_per_sec": 1.0, "retrans": 1, "timeouts": 1,
|
|
"tx_bps": 2048.0, "rx_bps": 40960.0,
|
|
"rtt_ms": 10.0, "exe_ms": 20.0, "read_ms": 20.0}
|
|
assert got["/nfs/idle"] == {"mount": "/nfs/idle", "ops_per_sec": 0.0, "retrans": 0, "timeouts": 0,
|
|
"tx_bps": 0.0, "rx_bps": 0.0}
|
|
assert set(got) == {"/nfs/data", "/nfs/idle"} # counters gone back, or no baseline yet
|
|
assert a.nfs_delays(cur, prev, 0) == []
|