Add host temperature telemetry and history

This commit is contained in:
2026-10-13 23:39:04 -04:00
parent 09396ab584
commit 171f657876
28 changed files with 1466 additions and 32 deletions
+6
View File
@@ -778,6 +778,12 @@ class AgentTests(unittest.TestCase):
else:
self.assertEqual(inventory["cpuUsagePercent"], usage)
def test_inventory_reports_optional_temperature_capability_and_empty_samples(self):
with patch.object(self.agent, "temperatures", return_value=[]):
inventory = self.agent.inventory()
self.assertEqual(inventory["temperatures"], [])
self.assertIn("host.temperatures", inventory["capabilities"])
def resource_container(self):
return {"Name": self.agent.container_name(INSTANCE, 1),
"Config": {"Labels": {module.LABEL + ".managed": "true", module.LABEL + ".instance": INSTANCE, module.LABEL + ".generation": "1"}},
+96
View File
@@ -0,0 +1,96 @@
import errno
import importlib.util
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location("ec_agent_temperatures", Path(__file__).resolve().parents[1] / "ec_agent.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
class TemperatureTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.hwmon = self.root / "class"
self.hwmon.mkdir()
self.patch = patch.object(module, "HWMON_ROOT", self.hwmon)
self.patch.start()
self.addCleanup(self.patch.stop)
self.agent = object.__new__(module.Agent)
def chip(self, number, device, readings):
physical = self.root / (device + str(number)) / "hwmon" / ("hwmon" + str(number))
physical.mkdir(parents=True)
(self.hwmon / physical.name).symlink_to(physical, target_is_directory=True)
(physical / "name").write_text(device)
for channel, label, value in readings:
(physical / (channel + "_input")).write_text(str(value))
if label is not None:
(physical / (channel + "_label")).write_text(label)
return physical
def test_ec2_sensors_and_units_without_guessing_unknown_devices(self):
self.chip(0, "nvme", [("temp1", "Composite", 50900), ("temp2", "Sensor 1", 50900)])
self.chip(1, "pch_cometlake", [("temp1", None, 62000)])
self.chip(2, "coretemp", [("temp1", "Package id 0", 65000), ("temp2", "Core 0", 63000)])
self.chip(3, "iwlwifi_1", [("temp1", None, "unavailable")])
samples = self.agent.temperatures()
self.assertEqual([s["celsius"] for s in samples], [50.9, 50.9, 62.0, 65.0, 63.0])
self.assertEqual([s["kind"] for s in samples], ["nvme", "nvme", "chipset", "cpu-package", "cpu-core"])
self.assertEqual(len({s["id"] for s in samples}), len(samples))
def test_fault_disabled_bad_value_and_enodata_do_not_lose_other_sensors(self):
chip = self.chip(0, "coretemp", [("temp" + str(i), "Core " + str(i), value)
for i, value in enumerate([65000, 60000, "bad", 500000, 61000, 63000, 0], 1)])
(chip / "temp1_fault").write_text("1")
(chip / "temp2_enable").write_text("0")
original = Path.read_text
def read(path, *args, **kwargs):
if path.name == "temp5_input":
raise OSError(errno.ENODATA, "No data available")
return original(path, *args, **kwargs)
with patch.object(Path, "read_text", read):
self.assertEqual([s["celsius"] for s in self.agent.temperatures()], [63.0, 0.0])
def test_amd_keeps_physical_die_readings_and_excludes_control_offsets(self):
self.chip(0, "k10temp", [("temp1", "Tctl", 85000), ("temp2", "Tdie", 65000), ("temp3", "Tccd1", 64000)])
self.assertEqual([s["label"] for s in self.agent.temperatures()], ["Tdie", "Tccd1"])
def test_sensor_identity_survives_hwmon_renumbering_and_separates_drives(self):
physical = self.chip(1, "nvme", [("temp1", "Composite", 50000)])
self.chip(2, "nvme", [("temp1", "Composite", 51000)])
before = {s["celsius"]: s["id"] for s in self.agent.temperatures()}
renamed = physical.with_name("hwmon9")
physical.rename(renamed)
(self.hwmon / "hwmon1").unlink()
(self.hwmon / "hwmon9").symlink_to(renamed, target_is_directory=True)
self.assertEqual(before, {s["celsius"]: s["id"] for s in self.agent.temperatures()})
self.assertEqual(len(set(before.values())), 2)
def test_missing_hwmon_and_disappearing_devices_are_optional(self):
self.assertEqual(self.agent.temperatures(), [])
(self.hwmon / "hwmon0").symlink_to(self.root / "missing")
self.assertEqual(self.agent.temperatures(), [])
with patch.object(Path, "glob", side_effect=PermissionError()):
self.assertEqual(self.agent.temperatures(), [])
def test_direct_hwmon_child_keeps_identity_when_renumbered(self):
physical = self.chip(1, "nvme", [("temp1", "Composite", 50000)])
direct = physical.parent.parent / "hwmon8"
physical.rename(direct)
(self.hwmon / "hwmon1").unlink()
(self.hwmon / "hwmon8").symlink_to(direct, target_is_directory=True)
before = self.agent.temperatures()[0]["id"]
renamed = direct.with_name("hwmon9")
direct.rename(renamed)
(self.hwmon / "hwmon8").unlink()
(self.hwmon / "hwmon9").symlink_to(renamed, target_is_directory=True)
self.assertEqual(self.agent.temperatures()[0]["id"], before)
def test_inventory_size_is_bounded(self):
self.chip(0, "coretemp", [("temp" + str(i), "Core " + str(i), 65000) for i in range(1, 200)])
self.assertEqual(len(self.agent.temperatures()), 128)