Files
wjones 41942eb9c9 Add advanced pattern detection features to Powerball reader
- Enhanced CSV parsing script to include advanced statistical analyses.
- Implemented functions to detect consecutive numbers, odd/even distributions, high/low distributions, sum patterns, gap patterns, hot/cold numbers, overdue numbers, number pairs, and decade distributions.
- Integrated pattern analysis results into combination generation for more strategic number selection.
- Updated main function to run advanced analyses and generate pattern-based combinations.
2026-06-24 14:35:29 -04:00

283 lines
9.4 KiB
Python

#!/usr/bin/env python3
import subprocess
import re
import sys
import datetime
import argparse
import configparser
import os
try:
from cryptography.fernet import Fernet, InvalidToken
except ImportError:
sys.exit("Error: 'cryptography' package is required. Run: pip install cryptography")
DEFAULT_EK = "0000000000000000000000000000000000000000"
DEFAULT_CREDENTIALS_FILE = "/root/cron_serverlists/credentials.ini"
DEFAULT_KEYFILE_NAME = "secret.key"
# IPMI settings (loaded at runtime from credentials file)
IPMIUSER = None
IPMIPW = None
IPMIEK = DEFAULT_EK
# Fan / temperature settings
FANSPEED = 15 # 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 is_fernet_token(value):
return isinstance(value, str) and value.startswith("gAAAA")
def load_fernet(keyfile):
if not os.path.exists(keyfile):
raise RuntimeError(f"key file not found: {keyfile}")
with open(keyfile, "rb") as fh:
key = fh.read()
try:
return Fernet(key)
except ValueError as e:
raise RuntimeError(f"invalid key format in key file: {keyfile}") from e
def decrypt_password(value, fernet):
# Support both encrypted and plaintext credentials for smoother migrations.
if not is_fernet_token(value):
return value
if fernet is None:
raise RuntimeError(
"encrypted password detected but key file was not found. "
"Use --keyfile /path/to/secret.key"
)
try:
return fernet.decrypt(value.encode()).decode()
except InvalidToken as e:
raise RuntimeError("failed to decrypt password; key file may not match credentials file") from e
def resolve_credentials_path(cli_path):
if cli_path and os.path.exists(cli_path):
return cli_path
candidates = []
if cli_path:
candidates.append(cli_path)
candidates.append(os.path.join(os.getcwd(), "credentials.ini"))
if DEFAULT_CREDENTIALS_FILE not in candidates:
candidates.append(DEFAULT_CREDENTIALS_FILE)
for candidate in candidates:
if os.path.exists(candidate):
return candidate
raise RuntimeError("credentials file not found. Looked in: " + ", ".join(candidates))
def resolve_keyfile(cli_keyfile, credentials_path):
if cli_keyfile:
return cli_keyfile if os.path.exists(cli_keyfile) else None
candidates = [
os.path.join(os.path.dirname(credentials_path), DEFAULT_KEYFILE_NAME),
os.path.join(os.path.dirname(DEFAULT_CREDENTIALS_FILE), DEFAULT_KEYFILE_NAME),
]
for candidate in candidates:
if os.path.exists(candidate):
return candidate
return None
def load_credentials(path, fernet):
if not os.path.exists(path):
raise RuntimeError(f"credentials file not found: {path}")
config = configparser.ConfigParser()
config.read(path)
try:
section = "ipmi_credentials"
username = config.get(section, "username", fallback="root")
encrypted_password = config.get(section, "password")
password = decrypt_password(encrypted_password, fernet)
encryption_key = config.get(
section,
"ipmiek",
fallback=config.get(section, "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=DEFAULT_CREDENTIALS_FILE,
help="Path to credentials file (default: %(default)s)",
)
parser.add_argument(
"--keyfile",
default=None,
help="Path to Fernet key file for encrypted passwords (default: auto-detect next to credentials)",
)
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
credentials_path = resolve_credentials_path(args.credentials)
keyfile_path = resolve_keyfile(args.keyfile, credentials_path)
fernet = load_fernet(keyfile_path) if keyfile_path else None
IPMIUSER, IPMIPW, IPMIEK = load_credentials(credentials_path, fernet)
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()