192 lines
6.6 KiB
Python
192 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
import subprocess
|
|
import re
|
|
import sys
|
|
import datetime
|
|
import argparse
|
|
import configparser
|
|
import os
|
|
|
|
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 = build_ipmi_base()
|
|
cmd = base + args
|
|
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
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)
|
|
except Exception:
|
|
# fallback to stdout
|
|
ts_print(msg)
|
|
|
|
def ts_print(msg):
|
|
"""ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
|
|
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
print(f"{now} {msg}")
|
|
|
|
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", 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, 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_exhaust_temp()
|
|
ts_print(f"{IPMIHOST}: Exhaust temperature: {temp} C")
|
|
except Exception as e:
|
|
ts_print(f"{IPMIHOST}: Error reading temperature: {e}")
|
|
sys.exit(1)
|
|
|
|
speed_hex = format(FANSPEED, "x")
|
|
|
|
if temp > MAXTEMP:
|
|
msg = f"{IPMIHOST}:Warning: Temperature is too high! Activating dynamic fan control! ({temp} C)"
|
|
log_systemd("R710-IPMI-TEMP", msg)
|
|
ts_print(msg)
|
|
run_ipmi(["raw", "0x30", "0x30", "0x01", "0x01"])
|
|
else:
|
|
okmsg = f"{IPMIHOST}: Temperature is OK ({temp} C)"
|
|
log_systemd("R710-IPMI-TEMP", okmsg)
|
|
ts_print(okmsg)
|
|
ts_print(f"{IPMIHOST}: Activating manual fan speeds! (requested %d%%)" % FANSPEED)
|
|
run_ipmi(["raw", "0x30", "0x30", "0x01", "0x00"])
|
|
run_ipmi(["raw", "0x30", "0x30", "0x02", "0xff", "0x" + speed_hex])
|
|
|
|
if __name__ == "__main__":
|
|
main()
|