Update set_fanspeed.py

This commit is contained in:
2026-03-21 10:05:52 -04:00
parent b38067dd8c
commit b8b585d242
+130 -24
View File
@@ -4,24 +4,55 @@ import re
import sys
import datetime
import argparse
import configparser
import os
# IPMI settings
IPMIUSER = "<username>"
IPMIPW = "<password>"
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 '<empty>'}\n"
f"stderr: {(p.stderr or '').strip() or '<empty>'}"
)
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 "<empty>",
"STDERR:",
err.strip() or "<empty>",
]
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}")