Add various scripts for server power management and build process
- Introduced .gitignore to exclude unnecessary files - Added scripts for powering on/off servers via IPMI and iLO - Implemented multi_powercycle script to manage multiple servers - Created build script for packaging multi_powercycle with PyInstaller - Enhanced logging functionality across scripts Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
+26
@@ -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
|
||||||
+13
-4
@@ -5,6 +5,15 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import urllib3
|
import urllib3
|
||||||
import redfish
|
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
|
# Suppress SSL warnings for self-signed iLO certificates
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
@@ -22,7 +31,7 @@ args = parser.parse_args()
|
|||||||
# password = yourpassword
|
# password = yourpassword
|
||||||
cred_path = args.credentials
|
cred_path = args.credentials
|
||||||
if not os.path.exists(cred_path):
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
@@ -31,7 +40,7 @@ try:
|
|||||||
LOGIN_ACCOUNT = config.get("ilo_credentials", "username", fallback="root")
|
LOGIN_ACCOUNT = config.get("ilo_credentials", "username", fallback="root")
|
||||||
LOGIN_PASSWORD = config.get("ilo_credentials", "password")
|
LOGIN_PASSWORD = config.get("ilo_credentials", "password")
|
||||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
BASE_URL = f"https://{args.hostname}"
|
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
|
body = {"ResetType": "PushPowerButton"} # iLO 4: PushPowerButton = graceful shutdown; use "ForceOff" for hard power cut
|
||||||
response = REDFISHOBJ.post(reset_uri, body=body)
|
response = REDFISHOBJ.post(reset_uri, body=body)
|
||||||
|
|
||||||
print(f"Status: {response.status}")
|
log(f"Status: {response.status}")
|
||||||
if response.status >= 400:
|
if response.status >= 400:
|
||||||
print(f"Error: {response.read}")
|
log(f"Error: {response.read}", is_error=True)
|
||||||
|
|
||||||
REDFISHOBJ.logout()
|
REDFISHOBJ.logout()
|
||||||
+13
-4
@@ -5,6 +5,15 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import urllib3
|
import urllib3
|
||||||
import redfish
|
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
|
# Suppress SSL warnings for self-signed iLO certificates
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
@@ -22,7 +31,7 @@ args = parser.parse_args()
|
|||||||
# password = yourpassword
|
# password = yourpassword
|
||||||
cred_path = args.credentials
|
cred_path = args.credentials
|
||||||
if not os.path.exists(cred_path):
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
@@ -31,7 +40,7 @@ try:
|
|||||||
LOGIN_ACCOUNT = config.get("ilo", "username", fallback="root")
|
LOGIN_ACCOUNT = config.get("ilo", "username", fallback="root")
|
||||||
LOGIN_PASSWORD = config.get("ilo", "password")
|
LOGIN_PASSWORD = config.get("ilo", "password")
|
||||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
BASE_URL = f"https://{args.hostname}"
|
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
|
body = {"ResetType": "PushPowerButton"} # iLO 4: PushPowerButton = graceful shutdown; use "ForceOff" for hard power cut
|
||||||
response = REDFISHOBJ.post(reset_uri, body=body)
|
response = REDFISHOBJ.post(reset_uri, body=body)
|
||||||
|
|
||||||
print(f"Status: {response.status}")
|
log(f"Status: {response.status}")
|
||||||
if response.status >= 400:
|
if response.status >= 400:
|
||||||
print(f"Error: {response.read}")
|
log(f"Error: {response.read}", is_error=True)
|
||||||
|
|
||||||
REDFISHOBJ.logout()
|
REDFISHOBJ.logout()
|
||||||
@@ -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()
|
||||||
+13
-4
@@ -4,13 +4,22 @@ import configparser
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
DEFAULT_EK = "0000000000000000000000000000000000000000"
|
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):
|
def load_credentials(path):
|
||||||
if not os.path.exists(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)
|
sys.exit(1)
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
@@ -21,7 +30,7 @@ def load_credentials(path):
|
|||||||
password = config.get("credentials", "password")
|
password = config.get("credentials", "password")
|
||||||
encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK)
|
encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK)
|
||||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
return username, password, encryption_key
|
return username, password, encryption_key
|
||||||
@@ -52,10 +61,10 @@ def main():
|
|||||||
|
|
||||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
if proc.returncode != 0:
|
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)
|
sys.exit(proc.returncode)
|
||||||
|
|
||||||
print(proc.stdout.strip())
|
log(proc.stdout.strip())
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -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()
|
||||||
@@ -5,6 +5,15 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import urllib3
|
import urllib3
|
||||||
import redfish
|
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
|
# Suppress SSL warnings for self-signed iLO certificates
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
@@ -22,7 +31,7 @@ args = parser.parse_args()
|
|||||||
# password = yourpassword
|
# password = yourpassword
|
||||||
cred_path = args.credentials
|
cred_path = args.credentials
|
||||||
if not os.path.exists(cred_path):
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
@@ -31,7 +40,7 @@ try:
|
|||||||
LOGIN_ACCOUNT = config.get("ilo_credentials", "username", fallback="root")
|
LOGIN_ACCOUNT = config.get("ilo_credentials", "username", fallback="root")
|
||||||
LOGIN_PASSWORD = config.get("ilo_credentials", "password")
|
LOGIN_PASSWORD = config.get("ilo_credentials", "password")
|
||||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
BASE_URL = f"https://{args.hostname}"
|
BASE_URL = f"https://{args.hostname}"
|
||||||
@@ -46,6 +55,6 @@ REDFISHOBJ.login(auth="session")
|
|||||||
sys_response = REDFISHOBJ.get("/redfish/v1/Systems/1/")
|
sys_response = REDFISHOBJ.get("/redfish/v1/Systems/1/")
|
||||||
power_state = sys_response.dict.get("PowerState", "Unknown")
|
power_state = sys_response.dict.get("PowerState", "Unknown")
|
||||||
|
|
||||||
print(f"Power State: {power_state}")
|
log(f"Power State: {power_state}")
|
||||||
|
|
||||||
REDFISHOBJ.logout()
|
REDFISHOBJ.logout()
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user