"""Host/chassis temperature collectors: IPMI (iDRAC) + CPU coretemp. Used by the host-poller (separate process from the SAS poller). In-band IPMI via ipmitool (/dev/ipmi0) gives environmental sensors (Inlet/Exhaust/…); the kernel coretemp hwmon gives CPU package temps. IPMI is gated for serialization; coretemp is a cheap sysfs read. """ import asyncio import logging import os import re from collections import Counter from pathlib import Path from services.hwgate import gate logger = logging.getLogger(__name__) HWMON = Path("/sys/class/hwmon") async def read_ipmi_temps() -> list[dict]: """`ipmitool sdr type temperature` → [{name, temperature_c, status, kind}].""" try: async with gate(): proc = await asyncio.create_subprocess_exec( "ipmitool", "sdr", "type", "temperature", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) out, err = await proc.communicate() except FileNotFoundError: logger.warning("ipmitool not found") return [] if proc.returncode != 0: logger.warning("ipmitool failed: %s", err.decode(errors="replace").strip()) return [] return _parse_ipmi(out.decode(errors="replace")) def _parse_ipmi(text: str) -> list[dict]: # e.g. "Inlet Temp | 04h | ok | 7.1 | 23 degrees C" # "Temp | 0Eh | ok | 3.1 | 40 degrees C" (CPU1) sensors: list[dict] = [] for line in text.splitlines(): parts = [p.strip() for p in line.split("|")] if len(parts) < 5: continue name, _sid, status, entity, reading = parts[:5] # Skip relative/derived sensors (e.g. "P1 Therm Margin") — they read in # "degrees C" but are a distance-to-throttle, not an absolute temp. if "margin" in name.lower(): continue m = re.search(r"(-?\d+(?:\.\d+)?)\s*degrees?\s*C", reading, re.IGNORECASE) if not m: # "no reading", "disabled", "ns", etc. continue temp = float(m.group(1)) kind = "env" # Dell reports CPU temps as "Temp" under entity 3.x — relabel them. if name.lower() == "temp" and entity.startswith("3."): name = f"CPU {entity.split('.')[-1]}" kind = "cpu" sensors.append({ "name": name, "temperature_c": temp, "status": status or "ok", "kind": kind, }) return sensors def read_hwmon_env() -> list[dict]: """Read labelled temps from extra hwmon chips as env sensors. For boxes without IPMI (e.g. Dell workstations), the BIOS SMM interface (`dell_smm`) exposes CPU/ambient/SODIMM/board temps. Chips are configurable via HOST_HWMON_CHIPS (comma list; default "dell_smm"); set to "" to disable. Names are "