diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41fd561 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# PyInstaller +build/ +dist/ +*.spec + +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python +*.egg-info/ +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ + +# Credentials (never commit these) +credentials +*.credentials + +# OS +.DS_Store +Thumbs.db diff --git a/personal/cron_scripts/ilo_powercycle.py b/personal/cron_scripts/archived_versions/ilo_powercycle.py.04262026 similarity index 77% rename from personal/cron_scripts/ilo_powercycle.py rename to personal/cron_scripts/archived_versions/ilo_powercycle.py.04262026 index 972b361..01db684 100644 --- a/personal/cron_scripts/ilo_powercycle.py +++ b/personal/cron_scripts/archived_versions/ilo_powercycle.py.04262026 @@ -5,6 +5,15 @@ import os import sys import urllib3 import redfish +from datetime import datetime + +def log(message, is_error=False): + """Print message with timestamp""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if is_error: + print(f"[{timestamp}] {message}", file=sys.stderr) + else: + print(f"[{timestamp}] {message}") # Suppress SSL warnings for self-signed iLO certificates urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -22,7 +31,7 @@ args = parser.parse_args() # password = yourpassword cred_path = args.credentials if not os.path.exists(cred_path): - print(f"Error: credentials file not found: {cred_path}", file=sys.stderr) + log(f"Error: credentials file not found: {cred_path}", is_error=True) sys.exit(1) config = configparser.ConfigParser() @@ -31,7 +40,7 @@ try: LOGIN_ACCOUNT = config.get("ilo_credentials", "username", fallback="root") LOGIN_PASSWORD = config.get("ilo_credentials", "password") except (configparser.NoSectionError, configparser.NoOptionError) as e: - print(f"Error reading credentials file: {e}", file=sys.stderr) + log(f"Error reading credentials file: {e}", is_error=True) sys.exit(1) BASE_URL = f"https://{args.hostname}" @@ -50,8 +59,8 @@ reset_uri = sys_response.dict["Actions"]["#ComputerSystem.Reset"]["target"] body = {"ResetType": "PushPowerButton"} # iLO 4: PushPowerButton = graceful shutdown; use "ForceOff" for hard power cut response = REDFISHOBJ.post(reset_uri, body=body) -print(f"Status: {response.status}") +log(f"Status: {response.status}") if response.status >= 400: - print(f"Error: {response.read}") + log(f"Error: {response.read}", is_error=True) REDFISHOBJ.logout() \ No newline at end of file diff --git a/personal/cron_scripts/ilo_poweron.py b/personal/cron_scripts/archived_versions/ilo_poweron.py.04262026 similarity index 76% rename from personal/cron_scripts/ilo_poweron.py rename to personal/cron_scripts/archived_versions/ilo_poweron.py.04262026 index 4e07fc7..87c1a4e 100644 --- a/personal/cron_scripts/ilo_poweron.py +++ b/personal/cron_scripts/archived_versions/ilo_poweron.py.04262026 @@ -5,6 +5,15 @@ import os import sys import urllib3 import redfish +from datetime import datetime + +def log(message, is_error=False): + """Print message with timestamp""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if is_error: + print(f"[{timestamp}] {message}", file=sys.stderr) + else: + print(f"[{timestamp}] {message}") # Suppress SSL warnings for self-signed iLO certificates urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -22,7 +31,7 @@ args = parser.parse_args() # password = yourpassword cred_path = args.credentials if not os.path.exists(cred_path): - print(f"Error: credentials file not found: {cred_path}", file=sys.stderr) + log(f"Error: credentials file not found: {cred_path}", is_error=True) sys.exit(1) config = configparser.ConfigParser() @@ -31,7 +40,7 @@ try: LOGIN_ACCOUNT = config.get("ilo", "username", fallback="root") LOGIN_PASSWORD = config.get("ilo", "password") except (configparser.NoSectionError, configparser.NoOptionError) as e: - print(f"Error reading credentials file: {e}", file=sys.stderr) + log(f"Error reading credentials file: {e}", is_error=True) sys.exit(1) BASE_URL = f"https://{args.hostname}" @@ -50,8 +59,8 @@ reset_uri = sys_response.dict["Actions"]["#ComputerSystem.Reset"]["target"] body = {"ResetType": "PushPowerButton"} # iLO 4: PushPowerButton = graceful shutdown; use "ForceOff" for hard power cut response = REDFISHOBJ.post(reset_uri, body=body) -print(f"Status: {response.status}") +log(f"Status: {response.status}") if response.status >= 400: - print(f"Error: {response.read}") + log(f"Error: {response.read}", is_error=True) REDFISHOBJ.logout() \ No newline at end of file diff --git a/personal/cron_scripts/archived_versions/ipmi_multipoweron.py.04262026 b/personal/cron_scripts/archived_versions/ipmi_multipoweron.py.04262026 new file mode 100644 index 0000000..cf282b8 --- /dev/null +++ b/personal/cron_scripts/archived_versions/ipmi_multipoweron.py.04262026 @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +import argparse +import configparser +import os +import subprocess +import sys +import datetime + +# Examples: +# ./ipmi_multipoweron.py host1 host2 host3 +# ./ipmi_multipoweron.py host1 host2 host3 -c /path/to/credentials +# ./ipmi_multipoweron.py -f serverlist.txt +# ./ipmi_multipoweron.py -f serverlist.txt -c /path/to/credentials + +def ts_print(msg): + """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}") + +DEFAULT_EK = "0000000000000000000000000000000000000000" + + +def find_credentials_file(provided_path): + """Find credentials file in standard locations if provided path doesn't exist.""" + # If provided path exists, use it + if os.path.exists(provided_path): + return provided_path + + # Check standard locations + standard_paths = [ + os.path.expanduser("~/credentials"), + "/etc/credentials", + ] + + for path in standard_paths: + if os.path.exists(path): + return path + + # Return the provided path (will fail with proper error message later) + return provided_path + + +def load_credentials(path): + cred_path = find_credentials_file(path) + + if not os.path.exists(cred_path): + ts_print(f"Error: credentials file not found: {cred_path}") + sys.exit(1) + + config = configparser.ConfigParser() + config.read(cred_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: + ts_print(f"Error reading credentials file: {e}") + sys.exit(1) + + return username, password, encryption_key + + +def power_on_host(host, ipmi_user, ipmi_pw, ipmi_ek): + """Power on a single host via IPMI. Returns True if successful.""" + cmd = [ + "ipmitool", + "-I", "lanplus", + "-H", host, + "-U", ipmi_user, + "-P", ipmi_pw, + "-y", ipmi_ek, + "power", "on", + ] + + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if proc.returncode != 0: + ts_print(f"ERROR: Failed to power on {host}: {proc.stderr.strip()}") + return False + else: + ts_print(f"SUCCESS: {host} - {proc.stdout.strip()}") + return True + + +def load_servers_from_file(filepath): + """Load server list from a file (one server per line, lines starting with # are ignored).""" + if not os.path.exists(filepath): + ts_print(f"Error: server list file not found: {filepath}") + sys.exit(1) + + servers = [] + try: + with open(filepath, 'r') as f: + for line in f: + line = line.strip() + # Skip empty lines and comments + if line and not line.startswith('#'): + servers.append(line) + except Exception as e: + ts_print(f"Error reading server list file: {e}") + sys.exit(1) + + return servers + + +def main(): + p = argparse.ArgumentParser(description="Send IPMI power on to multiple servers") + + # Either provide servers as arguments or a file + group = p.add_mutually_exclusive_group(required=True) + group.add_argument( + "servers", + nargs="*", + help="List of IPMI host addresses (space-separated)" + ) + group.add_argument( + "-f", "--file", + help="Path to file containing list of servers (one per line)" + ) + + p.add_argument( + "-c", + "--credentials", + default="credentials", + help="Path to credentials file (default: credentials; also checks ~/ and /etc/ if not found)", + ) + args = p.parse_args() + + # Determine server list + if args.file: + servers = load_servers_from_file(args.file) + else: + servers = args.servers + + if not servers: + ts_print("Error: No servers specified") + sys.exit(1) + + ts_print(f"Starting power on sequence for {len(servers)} server(s)") + + ipmi_user, ipmi_pw, ipmi_ek = load_credentials(args.credentials) + + success_count = 0 + failed_count = 0 + + for host in servers: + if power_on_host(host, ipmi_user, ipmi_pw, ipmi_ek): + success_count += 1 + else: + failed_count += 1 + + ts_print(f"Completed: {success_count} successful, {failed_count} failed") + + if failed_count > 0: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/personal/cron_scripts/ipmi_poweroff.py b/personal/cron_scripts/archived_versions/ipmi_poweroff.py.04262026 similarity index 73% rename from personal/cron_scripts/ipmi_poweroff.py rename to personal/cron_scripts/archived_versions/ipmi_poweroff.py.04262026 index 9847003..c1fb831 100644 --- a/personal/cron_scripts/ipmi_poweroff.py +++ b/personal/cron_scripts/archived_versions/ipmi_poweroff.py.04262026 @@ -4,13 +4,22 @@ import configparser import os import subprocess import sys +from datetime import datetime DEFAULT_EK = "0000000000000000000000000000000000000000" +def log(message, is_error=False): + """Print message with timestamp""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if is_error: + print(f"[{timestamp}] {message}", file=sys.stderr) + else: + print(f"[{timestamp}] {message}") + def load_credentials(path): if not os.path.exists(path): - print(f"Error: credentials file not found: {path}", file=sys.stderr) + log(f"Error: credentials file not found: {path}", is_error=True) sys.exit(1) config = configparser.ConfigParser() @@ -21,7 +30,7 @@ def load_credentials(path): password = config.get("credentials", "password") encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK) except (configparser.NoSectionError, configparser.NoOptionError) as e: - print(f"Error reading credentials file: {e}", file=sys.stderr) + log(f"Error reading credentials file: {e}", is_error=True) sys.exit(1) return username, password, encryption_key @@ -52,10 +61,10 @@ def main(): 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) + log("ipmitool failed: " + proc.stderr.strip(), is_error=True) sys.exit(proc.returncode) - print(proc.stdout.strip()) + log(proc.stdout.strip()) if __name__ == "__main__": main() diff --git a/personal/cron_scripts/ipmi_poweron.py b/personal/cron_scripts/archived_versions/ipmi_poweron.py.04262026 similarity index 100% rename from personal/cron_scripts/ipmi_poweron.py rename to personal/cron_scripts/archived_versions/ipmi_poweron.py.04262026 diff --git a/personal/cron_scripts/build_multi_powercycle.py b/personal/cron_scripts/build_multi_powercycle.py new file mode 100644 index 0000000..83e731b --- /dev/null +++ b/personal/cron_scripts/build_multi_powercycle.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +import subprocess +import sys +from pathlib import Path + + +SCRIPT_DIR = Path(__file__).resolve().parent +ENTRYPOINT = SCRIPT_DIR / "multi_powercycle.py" +DIST_DIR = SCRIPT_DIR / "dist" + + +def main(): + command = [ + sys.executable, + "-m", + "PyInstaller", + "--noconfirm", + "--clean", + "--onefile", + "--name", + "multi_powercycle", + "--collect-submodules", + "redfish", + str(ENTRYPOINT), + ] + + try: + subprocess.run(command, cwd=SCRIPT_DIR, check=True) + except subprocess.CalledProcessError as exc: + raise SystemExit(exc.returncode) from exc + + output_name = "multi_powercycle.exe" if sys.platform.startswith("win") else "multi_powercycle" + output_path = DIST_DIR / output_name + print(f"Built executable: {output_path}") + print("Build once on each target OS to get a native executable for that platform.") + + +if __name__ == "__main__": + main() diff --git a/personal/cron_scripts/ilo_status.py b/personal/cron_scripts/ilo_status.py index 47a277a..721bcb1 100644 --- a/personal/cron_scripts/ilo_status.py +++ b/personal/cron_scripts/ilo_status.py @@ -5,6 +5,15 @@ import os import sys import urllib3 import redfish +from datetime import datetime + +def log(message, is_error=False): + """Print message with timestamp""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if is_error: + print(f"[{timestamp}] {message}", file=sys.stderr) + else: + print(f"[{timestamp}] {message}") # Suppress SSL warnings for self-signed iLO certificates urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -22,7 +31,7 @@ args = parser.parse_args() # password = yourpassword cred_path = args.credentials if not os.path.exists(cred_path): - print(f"Error: credentials file not found: {cred_path}", file=sys.stderr) + log(f"Error: credentials file not found: {cred_path}", is_error=True) sys.exit(1) config = configparser.ConfigParser() @@ -31,7 +40,7 @@ try: LOGIN_ACCOUNT = config.get("ilo_credentials", "username", fallback="root") LOGIN_PASSWORD = config.get("ilo_credentials", "password") except (configparser.NoSectionError, configparser.NoOptionError) as e: - print(f"Error reading credentials file: {e}", file=sys.stderr) + log(f"Error reading credentials file: {e}", is_error=True) sys.exit(1) BASE_URL = f"https://{args.hostname}" @@ -46,6 +55,6 @@ REDFISHOBJ.login(auth="session") sys_response = REDFISHOBJ.get("/redfish/v1/Systems/1/") power_state = sys_response.dict.get("PowerState", "Unknown") -print(f"Power State: {power_state}") +log(f"Power State: {power_state}") REDFISHOBJ.logout() diff --git a/personal/cron_scripts/multi_powercycle.py b/personal/cron_scripts/multi_powercycle.py new file mode 100644 index 0000000..0c17a4c --- /dev/null +++ b/personal/cron_scripts/multi_powercycle.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +import argparse +import configparser +import os +import subprocess +import sys +from datetime import datetime + +import redfish +import urllib3 + +DEFAULT_CREDENTIALS_FILE = "credentials" +DEFAULT_SERVER_LIST_FILE = "/root/crontab_serverlists/serverlist.ini" + +# Examples: +# ./multi_powercycle.py on +# ./multi_powercycle.py off +# ./multi_powercycle.py on -f server_groups.txt +# +# File format: +# [ipmi] +# 192.168.129.240 +# 192.168.129.241 +# +# [ilo] +# 192.168.129.242 + +DEFAULT_EK = "0000000000000000000000000000000000000000" + + +def log(message, is_error=False): + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + stream = sys.stderr if is_error else sys.stdout + print(f"[{timestamp}] {message}", file=stream) + + +def parse_server_groups(path): + if not os.path.exists(path): + log(f"Error: server list file not found: {path}", is_error=True) + sys.exit(1) + + groups = {"ipmi": [], "ilo": []} + current_group = None + aliases = { + "ipmi": "ipmi", + "dell": "ipmi", + "ilo": "ilo", + } + + try: + with open(path, "r", encoding="utf-8") as handle: + for raw_line in handle: + line = raw_line.strip() + if not line or line.startswith("#"): + continue + + if line.startswith("[") and line.endswith("]"): + section_name = line[1:-1].strip().lower() + current_group = aliases.get(section_name) + if current_group is None: + log(f"Error: unsupported server group [{section_name}] in {path}", is_error=True) + sys.exit(1) + continue + + if current_group is None: + log(f"Error: host entry found before any section header in {path}: {line}", is_error=True) + sys.exit(1) + + groups[current_group].append(line) + except OSError as exc: + log(f"Error reading server list file: {exc}", is_error=True) + sys.exit(1) + + return groups + + +def load_ipmi_credentials(path): + if not os.path.exists(path): + log(f"Error: IPMI credentials file not found: {path}", is_error=True) + sys.exit(1) + + config = configparser.ConfigParser() + config.read(path) + + section = "ipmi_credentials" + if not config.has_section(section): + log(f"Error reading IPMI credentials file: missing [{section}] section in {path}", is_error=True) + sys.exit(1) + + username = config.get(section, "username", fallback="root") + password = config.get(section, "password", fallback=None) + encryption_key = config.get( + section, + "ipmiek", + fallback=config.get(section, "encryption_key", fallback=DEFAULT_EK), + ) + + if not password: + log(f"Error reading IPMI credentials file: missing password in [{section}] section of {path}", is_error=True) + sys.exit(1) + + return username, password, encryption_key + + +def load_ilo_credentials(path): + if not os.path.exists(path): + log(f"Error: iLO credentials file not found: {path}", is_error=True) + sys.exit(1) + + config = configparser.ConfigParser() + config.read(path) + + section = "ilo_credentials" + if not config.has_section(section): + log(f"Error reading iLO credentials file: missing [{section}] section in {path}", is_error=True) + sys.exit(1) + + username = config.get(section, "username", fallback="root") + password = config.get(section, "password", fallback=None) + if password: + return username, password + + log(f"Error reading iLO credentials file: missing password in [{section}] section of {path}", is_error=True) + sys.exit(1) + + +def ipmi_power(host, action, username, password, encryption_key): + cmd = [ + "ipmitool", + "-I", + "lanplus", + "-H", + host, + "-U", + username, + "-P", + password, + "-y", + encryption_key, + "power", + action, + ] + + log(f"Sending IPMI power {action} to {host}") + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if proc.returncode != 0: + detail = proc.stderr.strip() or proc.stdout.strip() or "unknown error" + log(f"IPMI {action} failed for {host}: {detail}", is_error=True) + return False + + detail = proc.stdout.strip() or f"power {action} request accepted" + log(f"IPMI {action} succeeded for {host}: {detail}") + return True + + +def ilo_powercycle(host, username, password): + base_url = f"https://{host}" + client = redfish.RedfishClient(base_url=base_url, username=username, password=password) + + try: + client.login(auth="session") + sys_response = client.get("/redfish/v1/Systems/1/") + reset_uri = sys_response.dict["Actions"]["#ComputerSystem.Reset"]["target"] + response = client.post(reset_uri, body={"ResetType": "PushPowerButton"}) + + log(f"iLO powercycle status for {host}: {response.status}") + if response.status >= 400: + log(f"iLO powercycle failed for {host}: {response.read}", is_error=True) + return False + return True + except Exception as exc: + log(f"iLO powercycle failed for {host}: {exc}", is_error=True) + return False + finally: + try: + client.logout() + except Exception: + pass + + +def main(): + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + parser = argparse.ArgumentParser( + description="Dispatch power operations to IPMI hosts and iLO hosts from a grouped server list" + ) + parser.add_argument("action", choices=("on", "off"), help="Power action for IPMI hosts") + parser.add_argument( + "-f", + "--file", + dest="server_file", + default=DEFAULT_SERVER_LIST_FILE, + help=f"Path to server list file (default: {DEFAULT_SERVER_LIST_FILE})", + ) + parser.add_argument( + "--ipmi-credentials", + default=DEFAULT_CREDENTIALS_FILE, + help=f"Path to IPMI credentials file (default: {DEFAULT_CREDENTIALS_FILE})", + ) + parser.add_argument( + "--ilo-credentials", + default=DEFAULT_CREDENTIALS_FILE, + help=f"Path to iLO credentials file (default: {DEFAULT_CREDENTIALS_FILE})", + ) + args = parser.parse_args() + + groups = parse_server_groups(args.server_file) + total_hosts = len(groups["ipmi"]) + len(groups["ilo"]) + if total_hosts == 0: + log(f"Error: no servers found in {args.server_file}", is_error=True) + sys.exit(1) + + log( + f"Starting run for {total_hosts} host(s): {len(groups['ipmi'])} IPMI host(s), {len(groups['ilo'])} iLO host(s)" + ) + + ipmi_creds = None + ilo_creds = None + failures = 0 + + if groups["ipmi"]: + ipmi_creds = load_ipmi_credentials(args.ipmi_credentials) + for host in groups["ipmi"]: + if not ipmi_power(host, args.action, *ipmi_creds): + failures += 1 + + if groups["ilo"]: + ilo_creds = load_ilo_credentials(args.ilo_credentials) + for host in groups["ilo"]: + if not ilo_powercycle(host, *ilo_creds): + failures += 1 + + successes = total_hosts - failures + log(f"Completed run: {successes} successful, {failures} failed") + if failures: + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file