From 95f132bdf8991846a5bb693f407ec7649500fbc7 Mon Sep 17 00:00:00 2001 From: Wendell Jones Date: Mon, 6 Oct 2025 10:10:54 -0400 Subject: [PATCH] Add scripts for managing VM and LXC container power states and fan control - Implemented set_fanspeed.py for dynamic fan speed control based on temperature. - Created ilo_poweroff.py and ilo_poweron.py for iLO power management. - Added ipmi_poweroff.py and ipmi_poweron.py for IPMI power control. - Developed vm_start.py, vm_stop.py, vm_restart.py, and vm_shutdown.py for VM and LXC management. - Introduced wrapper scripts (start_wrapper.sh, stop_wrapper.sh) for simplified execution. - Removed deprecated shell scripts for power control. --- .../cron_scripts/fancontrol/set_fanspeed.py | 85 +++++++++++++++++++ personal/cron_scripts/power/ilo_poweroff.py | 38 +++++++++ personal/cron_scripts/power/ilo_poweron.py | 35 ++++++++ personal/cron_scripts/power/ipmi_poweroff.py | 31 +++++++ personal/cron_scripts/power/ipmi_poweron.py | 38 +++++++++ .../system/start_wrapper.sh | 0 .../{ => cron_scripts}/system/stop_wrapper.sh | 0 .../system}/vm_powerup.py | 0 .../{ => cron_scripts}/system/vm_restart.py | 0 .../system/vm_shutdown.py.v1 | 0 .../{ => cron_scripts}/system/vm_start.py | 0 personal/{ => cron_scripts}/system/vm_stop.py | 0 .../{ => cron_scripts}/system/vm_stop.py.v1 | 0 personal/fancontrol/setspeed_fanspeed.sh | 41 --------- personal/power/poweroff.sh | 2 - personal/power/poweron.sh | 2 - 16 files changed, 227 insertions(+), 45 deletions(-) create mode 100644 personal/cron_scripts/fancontrol/set_fanspeed.py create mode 100644 personal/cron_scripts/power/ilo_poweroff.py create mode 100644 personal/cron_scripts/power/ilo_poweron.py create mode 100644 personal/cron_scripts/power/ipmi_poweroff.py create mode 100644 personal/cron_scripts/power/ipmi_poweron.py rename personal/{ => cron_scripts}/system/start_wrapper.sh (100%) rename personal/{ => cron_scripts}/system/stop_wrapper.sh (100%) rename personal/{power => cron_scripts/system}/vm_powerup.py (100%) rename personal/{ => cron_scripts}/system/vm_restart.py (100%) rename personal/{ => cron_scripts}/system/vm_shutdown.py.v1 (100%) rename personal/{ => cron_scripts}/system/vm_start.py (100%) rename personal/{ => cron_scripts}/system/vm_stop.py (100%) rename personal/{ => cron_scripts}/system/vm_stop.py.v1 (100%) delete mode 100644 personal/fancontrol/setspeed_fanspeed.sh delete mode 100644 personal/power/poweroff.sh delete mode 100644 personal/power/poweron.sh diff --git a/personal/cron_scripts/fancontrol/set_fanspeed.py b/personal/cron_scripts/fancontrol/set_fanspeed.py new file mode 100644 index 0000000..32de6c9 --- /dev/null +++ b/personal/cron_scripts/fancontrol/set_fanspeed.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +import subprocess +import re +import sys +import datetime +import argparse + +# IPMI settings +IPMIUSER = "" +IPMIPW = "" +IPMIEK = "0000000000000000000000000000000000000000" + +# Fan / temperature settings +FANSPEED = 20 # percent +MAXTEMP = 37 # Celsius + +def run_ipmi(args): + base = ["ipmitool", "-I", "lanplus", "-H", IPMIHOST, "-U", IPMIUSER, "-P", IPMIPW, "-y", IPMIEK] + 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()}") + return p.stdout + +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 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 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)") + args = parser.parse_args() + + global IPMIHOST + # set global IPMIHOST so run_ipmi() uses the provided host + IPMIHOST = args.host + + ts_print(f"{IPMIHOST}: Fan speed check for host {IPMIHOST}") + try: + temp = get_ambient_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() diff --git a/personal/cron_scripts/power/ilo_poweroff.py b/personal/cron_scripts/power/ilo_poweroff.py new file mode 100644 index 0000000..57c1615 --- /dev/null +++ b/personal/cron_scripts/power/ilo_poweroff.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import argparse +import subprocess +import getpass +import sys +import datetime + +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 run(cmd): + try: + subprocess.run(cmd, check=True) + except subprocess.CalledProcessError as e: + print(f"Command failed: {' '.join(cmd)}", file=sys.stderr) + sys.exit(e.returncode) + +def main(): + p = argparse.ArgumentParser(description="Run iLO REST commands (login, serverinfo, reboot)") + p.add_argument("-H", "--host", required=True, help="iLO host/IP") + p.add_argument("-a", "--action", default="ForceOff", choices=["ForceOff", "GracefulRestart", "Reset"], + help="reboot action to run (default: ForceOff)") + p.add_argument("--no-serverinfo", action="store_true", help="skip running serverinfo") + args = p.parse_args() + + # login + ts_print(f"Sending {args.action} to {args.host}") + run(['ilorest', 'login', args.host, '-u', '', '-p', '']) + # optional serverinfo + if not args.no_serverinfo: + run(['ilorest', 'serverinfo']) + # power off / reboot action + run(['ilorest', 'reboot', args.action]) + +if __name__ == "__main__": + main() diff --git a/personal/cron_scripts/power/ilo_poweron.py b/personal/cron_scripts/power/ilo_poweron.py new file mode 100644 index 0000000..d5fd1aa --- /dev/null +++ b/personal/cron_scripts/power/ilo_poweron.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import argparse +import subprocess +import getpass +import sys +import datetime + +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 run(cmd): + try: + subprocess.run(cmd, check=True) + except subprocess.CalledProcessError as e: + print(f"Command failed: {' '.join(cmd)}", file=sys.stderr) + sys.exit(e.returncode) + +def main(): + p = argparse.ArgumentParser(description="Login to iLO, show serverinfo and power on") + p.add_argument("-H", "--host", required=True, help="iLO host/IP") + p.add_argument("-a", "--action", default="On", choices=["On","Off","GracefulRestart","ForceOff","Reset"], + help="power/reboot action to run (default: On)") + p.add_argument("--no-serverinfo", action="store_true", help="skip running serverinfo") + args = p.parse_args() + + ts_print(f"Sending {args.action} to {args.host}") + run(['ilorest', 'login', args.host, '-u', '', '-p', '']) + if not args.no_serverinfo: + run(['ilorest', 'serverinfo']) + run(['ilorest', 'reboot', args.action]) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/personal/cron_scripts/power/ipmi_poweroff.py b/personal/cron_scripts/power/ipmi_poweroff.py new file mode 100644 index 0000000..49ce33e --- /dev/null +++ b/personal/cron_scripts/power/ipmi_poweroff.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +import argparse +import subprocess +import sys + +IPMIUSER = "" +IPMIPW = "" +IPMIEK = "0000000000000000000000000000000000000000" + +def main(): + p = argparse.ArgumentParser(description="Send IPMI power off") + p.add_argument("-H", "--host", required=True, help="IPMI host address") + args = p.parse_args() + + cmd = [ + "ipmitool", + "-I", "lanplus", + "-H", args.host, + "-U", IPMIUSER, + "-P", IPMIPW, + "-y", IPMIEK, + "power", "off", + ] + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if proc.returncode != 0: + print("ipmitool failed:", proc.stderr.strip(), file=sys.stderr) + sys.exit(proc.returncode) + print(proc.stdout.strip()) + +if __name__ == "__main__": + main() diff --git a/personal/cron_scripts/power/ipmi_poweron.py b/personal/cron_scripts/power/ipmi_poweron.py new file mode 100644 index 0000000..18f9f75 --- /dev/null +++ b/personal/cron_scripts/power/ipmi_poweron.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import argparse +import subprocess +import sys +import datetime + +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}") + +IPMIUSER = "" +IPMIPW = "" +IPMIEK = "0000000000000000000000000000000000000000" + +def main(): + p = argparse.ArgumentParser(description="Send IPMI power off") + p.add_argument("-H", "--host", required=True, help="IPMI host address") + args = p.parse_args() + + cmd = [ + "ipmitool", + "-I", "lanplus", + "-H", args.host, + "-U", IPMIUSER, + "-P", IPMIPW, + "-y", IPMIEK, + "power", "on", + ] + print(f"Sending power on to {args.host}") + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if proc.returncode != 0: + print("ipmitool failed:", proc.stderr.strip(), file=sys.stderr) + sys.exit(proc.returncode) + print(proc.stdout.strip()) + +if __name__ == "__main__": + main() diff --git a/personal/system/start_wrapper.sh b/personal/cron_scripts/system/start_wrapper.sh similarity index 100% rename from personal/system/start_wrapper.sh rename to personal/cron_scripts/system/start_wrapper.sh diff --git a/personal/system/stop_wrapper.sh b/personal/cron_scripts/system/stop_wrapper.sh similarity index 100% rename from personal/system/stop_wrapper.sh rename to personal/cron_scripts/system/stop_wrapper.sh diff --git a/personal/power/vm_powerup.py b/personal/cron_scripts/system/vm_powerup.py similarity index 100% rename from personal/power/vm_powerup.py rename to personal/cron_scripts/system/vm_powerup.py diff --git a/personal/system/vm_restart.py b/personal/cron_scripts/system/vm_restart.py similarity index 100% rename from personal/system/vm_restart.py rename to personal/cron_scripts/system/vm_restart.py diff --git a/personal/system/vm_shutdown.py.v1 b/personal/cron_scripts/system/vm_shutdown.py.v1 similarity index 100% rename from personal/system/vm_shutdown.py.v1 rename to personal/cron_scripts/system/vm_shutdown.py.v1 diff --git a/personal/system/vm_start.py b/personal/cron_scripts/system/vm_start.py similarity index 100% rename from personal/system/vm_start.py rename to personal/cron_scripts/system/vm_start.py diff --git a/personal/system/vm_stop.py b/personal/cron_scripts/system/vm_stop.py similarity index 100% rename from personal/system/vm_stop.py rename to personal/cron_scripts/system/vm_stop.py diff --git a/personal/system/vm_stop.py.v1 b/personal/cron_scripts/system/vm_stop.py.v1 similarity index 100% rename from personal/system/vm_stop.py.v1 rename to personal/cron_scripts/system/vm_stop.py.v1 diff --git a/personal/fancontrol/setspeed_fanspeed.sh b/personal/fancontrol/setspeed_fanspeed.sh deleted file mode 100644 index 3669e1a..0000000 --- a/personal/fancontrol/setspeed_fanspeed.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# IPMI IP address -IPMIHOST= -# IPMI Username -IPMIUSER= -# Your IPMI Encryption Key -IPMIEK=0 -# Fan Speed / utilisation in percentage, for example 9 for 9% utilisation -# Please note that each fan can have a different rpm and will not all be the same speed -FANSPEED=10 - -# TEMPERATURE -# Change this to the temperature in Celsius you are comfortable with. -# If the temperature goes above the set degrees it will send raw IPMI command to enable dynamic fan control -MAXTEMP=30 - -# This variable sends a IPMI command to get the temperature, and outputs it as two digits. -# Do not edit unless you know what you do. -# Side note, if you are running ipmitool on the system you are controlling, you don't need to specify -H,-U,-P - from the OS installed on the host, ipmitool is assumed permitted. You only need host/user/pass for remote access. -TEMP=$(ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK sdr type temperature |grep Ambient |grep degrees |grep -Po '\d{2}' | tail -1) - -# Dont edit this, this converts decimal to hex -SPEEDHEX=$( printf "%x" $FANSPEED ) - -if [[ $TEMP > $MAXTEMP ]]; - then - printf "Warning: Temperature is too high! Activating dynamic fan control! ($TEMP C)" | systemd-cat -t R710-IPMI-TEMP - echo "Warning: Temperature is too high! Activating dynamic fan control! ($TEMP C)" - # This sets the fans to auto mode, so the motherboard will set it to a speed that it will need do cool the server down - ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK raw 0x30 0x30 0x01 0x01 - else - printf "Temperature is OK ($TEMP C)" | systemd-cat -t R710-IPMI-TEMP - printf "Activating manual fan speeds! (1560 RPM)" - # This sets the fans to manual mode - ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK raw 0x30 0x30 0x01 0x00 - # This is where we set the slower, quite speed - ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK raw 0x30 0x30 0x02 0xff 0x$SPEEDHEX -fi diff --git a/personal/power/poweroff.sh b/personal/power/poweroff.sh deleted file mode 100644 index ba22fe6..0000000 --- a/personal/power/poweroff.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -ipmitool -I lanplus -H 192.168.128.223 -U root -P Optimus0329 -y 0000000000000000000000000000000000000000 power off \ No newline at end of file diff --git a/personal/power/poweron.sh b/personal/power/poweron.sh deleted file mode 100644 index b01fe91..0000000 --- a/personal/power/poweron.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -ipmitool -I lanplus -H 192.168.128.223 -U root -P Optimus0329 -y 0000000000000000000000000000000000000000 power on \ No newline at end of file