Add host-poller: chassis IPMI/iDRAC + CPU + on-metal drive temps
New third producer container (host-poller) owning the server chassis subsystem, separate from the SAS-shelf poller: - services/host_sensors.py: in-band IPMI (ipmitool sdr) for Inlet/Exhaust/etc. + CPU package temps from coretemp hwmon - host_poller.py: own single-instance lock + heartbeat (host_poll keys), restart-storm guard, paced; writes jbod:host_sensors + jbod:host_drives - move host/on-metal drive collection off the SAS poller to host-poller (reads jbod:zfs_map for annotation) - store: generalized lock/meta with a key param; host_sensors + host-poll keys - mqtt_publisher: publish env/CPU/host-drive temps as entities on the hub device (JBOD Monitor) - Dockerfile host-poller target (ipmitool+smartmontools); compose.infra adds host-poller; build.sh gains host-poller/all
This commit is contained in:
@@ -19,6 +19,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from services import store
|
||||
from services.secrets import read_kv2
|
||||
from services.temps import build_temp_snapshot, get_node_id
|
||||
|
||||
@@ -242,6 +243,61 @@ class MqttPublisher:
|
||||
self._publish_discovery(enc)
|
||||
self._publish_state(enc)
|
||||
|
||||
def publish_host(self, sensors: dict, drives: list) -> None:
|
||||
"""Publish chassis temps (IPMI env, CPU, on-metal drives) on the hub."""
|
||||
self._publish_hub()
|
||||
state_topic = f"{self.base_topic}/{self.node}/host/state"
|
||||
device = self._hub_device_block()
|
||||
payload: dict = {}
|
||||
|
||||
def announce(object_id: str, cfg: dict) -> None:
|
||||
uid = cfg["unique_id"]
|
||||
if uid not in self._discovered:
|
||||
topic = f"{self.discovery_prefix}/sensor/{self.hub_id}/{object_id}/config"
|
||||
self._client.publish(topic, json.dumps(cfg), qos=1, retain=True)
|
||||
self._discovered.add(uid)
|
||||
|
||||
common = {
|
||||
"device_class": "temperature",
|
||||
"unit_of_measurement": "°C",
|
||||
"state_class": "measurement",
|
||||
"state_topic": state_topic,
|
||||
"availability_topic": self.availability_topic,
|
||||
"device": device,
|
||||
}
|
||||
|
||||
def add(kind: str, name: str, temp, icon: str | None = None) -> None:
|
||||
if temp is None or not name:
|
||||
return
|
||||
key = f"{kind}_{_slug(name)}"
|
||||
payload[key] = temp
|
||||
cfg = {
|
||||
**common,
|
||||
"name": name,
|
||||
"unique_id": f"{self.hub_id}_{key}",
|
||||
"value_template": f"{{{{ value_json.{key} }}}}",
|
||||
}
|
||||
if icon:
|
||||
cfg["icon"] = icon
|
||||
announce(key, cfg)
|
||||
|
||||
for s in sensors.get("env", []):
|
||||
add("env", s.get("name"), s.get("temperature_c"))
|
||||
for c in sensors.get("cpu", []):
|
||||
add("cpu", c.get("name"), c.get("temperature_c"), icon="mdi:cpu-64-bit")
|
||||
|
||||
def add_drive(d: dict) -> None:
|
||||
dev = d.get("device")
|
||||
if dev:
|
||||
add("drive", f"Drive {dev}", d.get("temperature_c"), icon="mdi:harddisk")
|
||||
|
||||
for d in drives:
|
||||
add_drive(d)
|
||||
for pd in d.get("physical_drives", []):
|
||||
add_drive(pd)
|
||||
|
||||
self._client.publish(state_topic, json.dumps(payload), qos=0, retain=True)
|
||||
|
||||
async def run(self) -> None:
|
||||
"""Background loop: build a snapshot and publish on an interval."""
|
||||
await asyncio.sleep(3) # let the first SMART poll populate the cache
|
||||
@@ -249,9 +305,14 @@ class MqttPublisher:
|
||||
try:
|
||||
snapshot = await build_temp_snapshot()
|
||||
self.publish_snapshot(snapshot)
|
||||
host_sensors = await store.get_host_sensors() or {}
|
||||
host_drives = await store.get_host_drives() or []
|
||||
self.publish_host(host_sensors, host_drives)
|
||||
logger.info(
|
||||
"MQTT published temps for %d enclosure(s)",
|
||||
"MQTT published temps for %d enclosure(s) + host (%d env, %d cpu)",
|
||||
len(snapshot.get("enclosures", [])),
|
||||
len(host_sensors.get("env", [])),
|
||||
len(host_sensors.get("cpu", [])),
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user