diff --git a/personal/cron_scripts/fancontrol/set_fanspeed.py b/personal/cron_scripts/fancontrol/set_fanspeed.py index 32de6c9..d079cac 100644 --- a/personal/cron_scripts/fancontrol/set_fanspeed.py +++ b/personal/cron_scripts/fancontrol/set_fanspeed.py @@ -4,24 +4,55 @@ import re import sys import datetime import argparse +import configparser +import os -# IPMI settings -IPMIUSER = "" -IPMIPW = "" -IPMIEK = "0000000000000000000000000000000000000000" +DEFAULT_EK = "0000000000000000000000000000000000000000" + +# IPMI settings (loaded at runtime from credentials file) +IPMIUSER = None +IPMIPW = None +IPMIEK = DEFAULT_EK # Fan / temperature settings FANSPEED = 20 # percent MAXTEMP = 37 # Celsius +def build_ipmi_base(): + base = ["ipmitool", "-I", IPMIINTERFACE, "-H", IPMIHOST, "-U", IPMIUSER, "-P", IPMIPW, "-y", IPMIEK] + if IPMICIPHER is not None: + base += ["-C", str(IPMICIPHER)] + return base + +def has_ipmi_session_error(stderr_text): + s = (stderr_text or "").lower() + markers = [ + "unable to establish ipmi v2 / rmcp+ session", + "rakp", + "session timeout", + "authentication type", + "invalid user name", + ] + return any(marker in s for marker in markers) + def run_ipmi(args): - base = ["ipmitool", "-I", "lanplus", "-H", IPMIHOST, "-U", IPMIUSER, "-P", IPMIPW, "-y", IPMIEK] + base = build_ipmi_base() cmd = base + args p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - if p.returncode != 0: - raise RuntimeError(f"ipmitool failed: {' '.join(cmd)}\n{p.stderr.strip()}") + if p.returncode != 0 or has_ipmi_session_error(p.stderr): + raise RuntimeError( + f"ipmitool failed: {' '.join(cmd)}\n" + f"stdout: {(p.stdout or '').strip() or ''}\n" + f"stderr: {(p.stderr or '').strip() or ''}" + ) return p.stdout +def run_ipmi_raw(args): + base = build_ipmi_base() + cmd = base + args + p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + return cmd, p.returncode, p.stdout, p.stderr + def log_systemd(tag, msg): try: subprocess.run(["systemd-cat", "-t", tag], input=msg + "\n", text=True, check=True) @@ -34,33 +65,108 @@ def ts_print(msg): now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") print(f"{now} {msg}") -def get_ambient_temp(): - out = run_ipmi(["sdr", "type", "temperature"]) - # find lines like "Exhuat Temp | 29 degrees C" - lines = [l for l in out.splitlines() if "Exhaust" in l and "degrees" in l] - if not lines: - raise RuntimeError("Exhaust temperature line not found in ipmitool output") - # Extract last number found in the last matching line (like the original) - field = lines[-1].split("|")[4] - #ts_print("Debug: field =", field) - m = re.search(r"(\d{1,3})", field) - if not m: - raise RuntimeError("Could not parse temperature from line: " + lines[-1]) - return int(m.group(1)) +def load_credentials(path): + if not os.path.exists(path): + raise RuntimeError(f"credentials file not found: {path}") + + config = configparser.ConfigParser() + config.read(path) + + try: + username = config.get("credentials", "username", fallback="root") + password = config.get("credentials", "password") + encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK) + except (configparser.NoSectionError, configparser.NoOptionError) as e: + raise RuntimeError(f"error reading credentials file: {e}") + + return username, password, encryption_key + +def get_exhaust_temp(): + commands_to_try = [ + ["sensor", "get", "Exhaust Temp"], + ["sdr", "type", "temperature"], + ["sensor", "list"], + ["sdr", "elist", "full"], + ] + + debug_sections = [] + session_error_seen = False + for args in commands_to_try: + cmd, rc, out, err = run_ipmi_raw(args) + if has_ipmi_session_error(err): + session_error_seen = True + cmd_text = " ".join(cmd) + section = [ + f"Command: {cmd_text}", + f"Return code: {rc}", + "STDOUT:", + out.strip() or "", + "STDERR:", + err.strip() or "", + ] + debug_sections.append("\n".join(section)) + + # Parse explicit sensor-get output first. + if args[:2] == ["sensor", "get"] and out: + m = re.search(r"Sensor\s+Reading\s*:\s*(-?\d+(?:\.\d+)?)\b.*degrees\s*C", out, re.IGNORECASE) + if m: + return int(float(m.group(1))) + + # Parse table outputs that have pipe-delimited sensor rows. + for line in out.splitlines(): + parts = [p.strip() for p in line.split("|")] + if len(parts) < 2: + continue + + sensor_name = parts[0] + normalized_name = " ".join(sensor_name.lower().split()) + if not (normalized_name == "exhaust temp" or normalized_name.startswith("exhaust temp ")): + continue + + for part in parts[1:]: + m = re.search(r"(-?\d+(?:\.\d+)?)\s*degrees\s*C", part, re.IGNORECASE) + if m: + return int(float(m.group(1))) + + if session_error_seen: + raise RuntimeError( + "Failed to establish an IPMI session. " + "Check host/credentials, then try different options like " + "--interface lan --cipher 3 or --interface lanplus --cipher 17.\n\n" + + "Debug output from attempted commands:\n\n" + + "\n\n---\n\n".join(debug_sections) + ) + + raise RuntimeError( + "Failed to read Exhaust Temp from ipmitool output.\n" + + "Debug output from attempted commands:\n\n" + + "\n\n---\n\n".join(debug_sections) + ) def main(): # parse CLI args (IPMI host can be overridden with -H/--host) parser = argparse.ArgumentParser(description="Set fan speed via IPMI") - parser.add_argument("-H", "--host", default="127.0.0.1", help="IPMI host/IP (default: %(default)s)") + parser.add_argument("-H", "--host", required=True, help="IPMI host/IP") + parser.add_argument( + "-c", + "--credentials", + default="/etc/credentials", + help="Path to credentials file (default: %(default)s)", + ) + parser.add_argument("--interface", default="lanplus", choices=["lanplus", "lan"], help="IPMI interface (default: %(default)s)") + parser.add_argument("--cipher", type=int, default=None, help="Optional IPMI cipher suite ID (e.g. 3 or 17)") args = parser.parse_args() - global IPMIHOST - # set global IPMIHOST so run_ipmi() uses the provided host + global IPMIHOST, IPMIINTERFACE, IPMICIPHER, IPMIUSER, IPMIPW, IPMIEK + # set globals so run_ipmi() uses provided connection settings and credentials IPMIHOST = args.host + IPMIINTERFACE = args.interface + IPMICIPHER = args.cipher + IPMIUSER, IPMIPW, IPMIEK = load_credentials(args.credentials) ts_print(f"{IPMIHOST}: Fan speed check for host {IPMIHOST}") try: - temp = get_ambient_temp() + temp = get_exhaust_temp() ts_print(f"{IPMIHOST}: Exhaust temperature: {temp} C") except Exception as e: ts_print(f"{IPMIHOST}: Error reading temperature: {e}")