feat(hosts): the live strip shows the hottest sensor's temperature, get_host marks sensors that don't count, and the Monitors card's button reads "Add" (#6803)
- The host page's live strip gains a Temperature tile: temp_c_max (real thermometers only), its last 6 h and the sensor setting it, graded by Settings → Thresholds and counted in the header's worst reading, in °C. - MCP get_host: each temperature carries `counted`; an unwired input is `counted: false` with `not_counted_because`, listed after the real ones. host_agent registers the judge (judge_sensors), so core imports no plugin. - The Monitors card's "Add monitor" wrapped into two lines beside the uptime; it reads "Add", titled "Add a monitor for this host". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,8 @@ async def test_ansible_comes_before_the_docker_list(app, monkeypatch):
|
||||
# Monitors are added on their own form, filed under this host (#5277);
|
||||
# each row's own page has the labelled Edit/Pause/Delete.
|
||||
assert 'href="/monitors/new?host_id=h1"' in page
|
||||
# The card's header shares one line with the uptime: a one-word button keeps it whole.
|
||||
assert 'title="Add a monitor for this host">Add</a>' in page
|
||||
assert "row-actions" not in page and 'action="/monitors/add"' not in page
|
||||
|
||||
|
||||
|
||||
+16
-1
@@ -312,10 +312,25 @@ def test_get_host_groups_parts_by_kind_and_summarises_cores():
|
||||
assert got["disk_io"]["items"][0]["device"] == "sda"
|
||||
assert [d["device"] for d in got["drive_health"]["items"]] == ["nvme0", "sda"] # failing first
|
||||
assert got["temperatures"]["items"][0] == {"sensor": "Package id 0", "part": "temp:Package id 0",
|
||||
"temp_c": 61.0}
|
||||
"temp_c": 61.0, "counted": True}
|
||||
assert "other" not in got
|
||||
|
||||
|
||||
def test_get_host_marks_the_sensors_the_hosts_temperature_leaves_out(monkeypatch):
|
||||
"""An unwired input reading 115 °C is listed after the real sensors, with
|
||||
why it doesn't count, so a reader doesn't take it for heat."""
|
||||
from steward.mcp.tools import hosts
|
||||
monkeypatch.setattr(hosts, "_UNCOUNTED_SENSORS",
|
||||
lambda host, temps: {k: "reads a value no wired sensor gives"
|
||||
for k, c in temps.items() if c >= 100})
|
||||
got = hosts.agent_parts({"temp:SYSTIN": {"temp_c": 115.0}, "temp:Package id 0": {"temp_c": 61.0},
|
||||
"temp:nvme": {"temp_c": 40.0}}, "box", limit=10)["temperatures"]["items"]
|
||||
assert [t["sensor"] for t in got] == ["Package id 0", "nvme", "SYSTIN"]
|
||||
assert got[0]["counted"] is True and "not_counted_because" not in got[0]
|
||||
assert got[2]["counted"] is False
|
||||
assert got[2]["not_counted_because"] == "reads a value no wired sensor gives"
|
||||
|
||||
|
||||
def test_one_silent_agent_is_one_finding():
|
||||
"""#5721: VDNT-Docker03's silent agent read as three findings. Its agent-
|
||||
offline alert speaks for it, the stale-agent finding goes, and its rules
|
||||
|
||||
@@ -25,16 +25,16 @@ def test_worst_reading(readings, worst):
|
||||
assert worst_reading(readings, TH) == worst
|
||||
|
||||
|
||||
async def _vitals(app, *, cpu=40.0, mem=50.0, stale=False, seen_min=1):
|
||||
async def _vitals(app, *, cpu=40.0, mem=50.0, stale=False, seen_min=1, temp=None, hottest=None):
|
||||
readings = (("CPU", cpu, "cpu"), ("Memory", mem, "mem"), ("Disk /", 30.0, "disk"),
|
||||
("Load per core", 20, "load"))
|
||||
("Load per core", 20, "load"), ("Temperature", temp, "temp"))
|
||||
async with app.test_request_context("/plugins/host_agent/vitals/h1"):
|
||||
page = await render_plugin_template(
|
||||
app, "host_agent", "_host_vitals.html", reporting=True, stale=stale,
|
||||
reg=SimpleNamespace(host_id="h1", agent_version="1.9.0",
|
||||
last_seen_at=datetime.now(timezone.utc) - timedelta(minutes=seen_min)),
|
||||
cpu=cpu, mem=mem, disk_root=30.0, load_per_core=20,
|
||||
sparks={"cpu": "", "mem": "", "disk": "", "load": ""},
|
||||
cpu=cpu, mem=mem, disk_root=30.0, load_per_core=20, temp=temp, hottest=hottest,
|
||||
sparks={"cpu": "", "mem": "", "disk": "", "load": "", "temp": "<svg></svg>"},
|
||||
psi={"cpu": None, "mem": None, "io": None}, worst=worst_reading(readings, TH))
|
||||
return re.sub(r">\s+<", "><", " ".join(page.split()))
|
||||
|
||||
@@ -59,6 +59,22 @@ async def test_the_strip_at_the_top_of_the_page_leads_to_the_full_metrics(app):
|
||||
assert 'href="/plugins/host_agent/h1/"' in await _vitals(app)
|
||||
|
||||
|
||||
async def test_the_strip_shows_the_hottest_sensor_with_its_name_and_line(app):
|
||||
page = await _vitals(app, temp=62.0, hottest="Package id 0")
|
||||
assert "--tiles:5;" in page
|
||||
assert ('class="stat-tile stat-ok"><span class="stat-tile-label">Temperature</span>'
|
||||
'<span class="stat-tile-value">62°C</span>') in page
|
||||
assert '<svg></svg><span class="stat-tile-sensor" title="Package id 0">Package id 0</span>' in page
|
||||
without = await _vitals(app)
|
||||
assert "--tiles:4;" in without and ">Temperature<" not in without
|
||||
|
||||
|
||||
async def test_a_host_past_its_heat_threshold_says_so_in_degrees(app):
|
||||
page = await _vitals(app, temp=88.0, hottest="Package id 0") # crit at 85
|
||||
assert '<span class="tone-crit">Temperature 88°C</span> is past its critical threshold.' in page
|
||||
assert 'class="stat-tile stat-crit"><span class="stat-tile-label">Temperature</span>' in page
|
||||
|
||||
|
||||
async def test_a_stale_host_says_since_when_before_any_reading(app):
|
||||
page = await _vitals(app, mem=93.0, stale=True, seen_min=20)
|
||||
assert '<span class="badge badge-warn">Stale</span>' in page
|
||||
|
||||
Reference in New Issue
Block a user