Skip to content

Instantly share code, notes, and snippets.

@dmerner
Forked from HQJaTu/ipmi-updater.py
Last active May 4, 2023 13:10
Show Gist options
  • Save dmerner/26b61d5d7cd67753110eb63b83d67e90 to your computer and use it in GitHub Desktop.
Save dmerner/26b61d5d7cd67753110eb63b83d67e90 to your computer and use it in GitHub Desktop.
Supermicro IPMI certificate updater
#!/usr/bin/env python3
# vim: autoindent tabstop=4 shiftwidth=4 expandtab softtabstop=4 filetype=python
# This file is part of Supermicro IPMI certificate updater.
# Supermicro IPMI certificate updater is free software: you can
# redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright (c) Jari Turkia
import os
import argparse
import requests
import logging
from datetime import datetime
from lxml import etree
from urllib.parse import urlparse
from requests.auth import HTTPBasicAuth
REQUEST_TIMEOUT = 5.0
LOGIN_URL = '%s/cgi/login.cgi'
IPMI_CERT_INFO_URL = '%s/cgi/ipmi.cgi'
UPLOAD_CERT_URL = '%s/cgi/upload_ssl.cgi'
REBOOT_IPMI_URL = '%s/cgi/BMCReset.cgi'
CONFIG_CERT_URL = '%s/cgi/url_redirect.cgi?url_name=config_ssl'
REDFISH_ROOT = '%s/redfish/v1/'
REDFISH_SSL = '%sUpdateService/SSLCert/'
REDFISH_SSL_UPLOAD = '%sUpdateService/SSLCert/Actions/SSLCert.Upload'
REDFISH_BMC_REBOOT = '%sManagers/1/Actions/Manager.Reset'
def login(session, url, username, password):
"""
Log into IPMI interface
:param session: Current session object
:type session requests.session
:param url: base-URL to IPMI
:param username: username to use for logging in
:param password: password to use for logging in
:return: bool
"""
login_data = {
'name': username,
'pwd': password
}
login_url = LOGIN_URL % url
try:
result = session.post(login_url, login_data, timeout=REQUEST_TIMEOUT, verify=False)
except ConnectionError:
return False
if not result.ok:
return False
if '/cgi/url_redirect.cgi?url_name=mainmenu' not in result.text:
return False
return True
def get_ipmi_cert_info(session, url, model, user, password):
"""
Verify existing certificate information
:param session: Current session object
:type session requests.session
:param url: base-URL to IPMI
:return: dict
"""
timestamp = datetime.utcnow().strftime('%a %d %b %Y %H:%M:%S GMT')
if model == "X11":
try:
r = session.get(REDFISH_SSL % (REDFISH_ROOT % url), auth=HTTPBasicAuth(user, password), verify=False)
except ConnectionError:
return False
if not r.ok:
return False
data = r.json()
return {
'has_cert': True,
'valid_from': data['VaildFrom'], # Yes, Supermicro made a typo in their BMC API.
'valid_until': data['GoodTHRU']
}
cert_info_data = {
'SSL_STATUS.XML': '(0,0)',
'time_stamp': timestamp # 'Thu Jul 12 2018 19:52:48 GMT+0300 (FLE Daylight Time)'
}
#for cookie in session.cookies:
# print(cookie)
ipmi_info_url = IPMI_CERT_INFO_URL % url
try:
result = session.post(ipmi_info_url, cert_info_data, timeout=REQUEST_TIMEOUT, verify=False)
except ConnectionError:
return False
if not result.ok:
return False
root = etree.fromstring(result.text)
# <?xml> <IPMI> <SSL_INFO> <STATUS>
status = root.xpath('//IPMI/SSL_INFO/STATUS')
if not status:
return False
# Since xpath will return a list, just pick the first one from it.
status = status[0]
has_cert = int(status.get('CERT_EXIST'))
has_cert = bool(has_cert)
if has_cert:
valid_from = status.get('VALID_FROM')
valid_until = status.get('VALID_UNTIL')
return {
'has_cert': has_cert,
'valid_from': valid_from,
'valid_until': valid_until
}
def get_ipmi_cert_valid(session, url):
"""
Verify existing certificate information
:param session: Current session object
:type session requests.session
:param url: base-URL to IPMI
:return: bool
"""
timestamp = datetime.utcnow().strftime('%a %d %b %Y %H:%M:%S GMT')
cert_info_data = {
'SSL_VALIDATE.XML': '(0,0)',
'time_stamp': timestamp # 'Thu Jul 12 2018 19:52:48 GMT+0300 (FLE Daylight Time)'
}
#for cookie in session.cookies:
# print(cookie)
ipmi_info_url = IPMI_CERT_INFO_URL % url
try:
result = session.post(ipmi_info_url, cert_info_data, timeout=REQUEST_TIMEOUT, verify=False)
except ConnectionError:
return False
if not result.ok:
return False
root = etree.fromstring(result.text)
# <?xml> <IPMI> <SSL_INFO>
status = root.xpath('//IPMI/SSL_INFO')
if not status:
return False
# Since xpath will return a list, just pick the first one from it.
status = status[0]
valid_cert = int(status.get('VALIDATE'))
return bool(valid_cert)
def upload_cert(session, url, key_file, cert_file, model, user, password):
"""
Send X.509 certificate and private key to server
:param session: Current session object
:type session requests.session
:param url: base-URL to IPMI
:param key_file: filename to X.509 certificate private key
:param cert_file: filename to X.509 certificate PEM
:return:
"""
with open(key_file, 'rb') as filehandle:
key_data = filehandle.read()
with open(cert_file, 'rb') as filehandle:
cert_data = filehandle.read()
if model == "X10":
files_to_upload = [
('/tmp/cert.pem', ('cert.pem', cert_data, 'application/octet-stream')),
('/tmp/key.pem', ('key.pem', key_data, 'application/octet-stream'))
]
upload_cert_url = UPLOAD_CERT_URL % url
elif model == "X11":
upload_cert_url = REDFISH_SSL_UPLOAD % (REDFISH_ROOT % url)
files_to_upload = {
'cert_file' : ('fullchain.pem', open(cert_file, 'rb')),
'key_file' : ('privkey.pem', open(key_file, 'rb'))
}
try:
result = session.post(upload_cert_url, files=files_to_upload, timeout=REQUEST_TIMEOUT, auth=HTTPBasicAuth(user, password) if model == "X11" else None, verify=False)
except ConnectionError:
return False
if not result.ok:
return False
if model == "X10":
if 'Content-Type' not in result.headers.keys() or result.headers['Content-Type'] != 'text/html':
# On failure, Content-Type will be 'text/plain' and 'Transfer-Encoding' is 'chunked'
return False
if 'CONFPAGE_RESET' not in result.text:
return False
return True
def reboot_ipmi(session, url, model, user, password):
timestamp = datetime.utcnow().strftime('%a %d %b %Y %H:%M:%S GMT')
reboot_data = {
'time_stamp': timestamp # 'Thu Jul 12 2018 19:52:48 GMT+0300 (FLE Daylight Time)'
}
if model == "X10":
reboot_url = REBOOT_IPMI_URL % url
elif model == "X11":
reboot_url = REDFISH_BMC_REBOOT % (REDFISH_ROOT % url)
try:
result = session.post(reboot_url, reboot_data, timeout=REQUEST_TIMEOUT, auth=HTTPBasicAuth(user, password) if model == "X11" else None, verify=False)
except ConnectionError:
return False
if not result.ok:
return False
if model == "X10":
if '<STATE CODE="OK"/>' not in result.text:
return False
return True
def determine_model(session, url):
redfish_url = REDFISH_ROOT % url
try:
r = session.get(redfish_url, timeout=REQUEST_TIMEOUT, verify=False)
except ConnectionError:
exit(2)
if not r.ok:
exit(2)
data = r.json()
# The UpdateService methods are only available on newer X11 based boards
if "UpdateService" in data:
return "X11"
else:
return "X10"
def main():
parser = argparse.ArgumentParser(description='Update Supermicro IPMI SSL certificate')
parser.add_argument('--ipmi-url', required=True,
help='Supermicro IPMI 2.0 URL')
parser.add_argument('--key-file', required=True,
help='X.509 Private key filename')
parser.add_argument('--cert-file', required=True,
help='X.509 Certificate filename')
parser.add_argument('--username', required=True,
help='IPMI username with admin access')
parser.add_argument('--password', required=True,
help='IPMI user password')
parser.add_argument('--no-reboot', action='store_true',
help='The default is to reboot the IPMI after upload for the change to take effect.')
parser.add_argument('--quiet', action='store_true',
help='Do not output anything if successful')
args = parser.parse_args()
# Confirm args
if not os.path.isfile(args.key_file):
print("--key-file '%s' doesn't exist!" % args.key_file)
exit(2)
if not os.path.isfile(args.cert_file):
print("--cert-file '%s' doesn't exist!" % args.cert_file)
exit(2)
if args.ipmi_url[-1] == '/':
args.ipmi_url = args.ipmi_url[0:-1]
if not args.quiet:
# Enable reuest logging
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
# Start the operation
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
session = requests.session()
# First determine if we are X10 or X11
model = determine_model(session, args.ipmi_url)
if not args.quiet:
print("Board model is " + model)
if model == "X10":
if not login(session, args.ipmi_url, args.username, args.password):
print("Login failed. Cannot continue!")
exit(2)
# Set mandatory cookies:
url_parts = urlparse(args.ipmi_url)
# Cookie: langSetFlag=0; language=English; SID=<dynamic session ID here!>; mainpage=configuration; subpage=config_ssl
mandatory_cookies = {
'langSetFlag': '0',
'language': 'English',
'mainpage': 'configuration',
'subpage': 'config_ssl'
}
for cookie_name, cookie_value in mandatory_cookies.items():
session.cookies.set(cookie_name, cookie_value, domain=url_parts.hostname)
cert_info = get_ipmi_cert_info(session, args.ipmi_url, model, args.username, args.password)
if not cert_info:
print("Failed to extract certificate information from IPMI!")
if model == "X11":
print("Try checking either your IPMI network config or your IPMI license!")
exit(2)
if not args.quiet and cert_info['has_cert']:
print("There exists a certificate, which is valid until: %s" % cert_info['valid_until'])
# Go upload!
if not upload_cert(session, args.ipmi_url, args.key_file, args.cert_file, model, args.username, args.password):
print("Failed to upload X.509 files to IPMI!")
exit(2)
# Redfish currently doesn't have a way to download the certificate on X11 boards
if model == "X10":
cert_valid = get_ipmi_cert_valid(session, args.ipmi_url)
if not cert_valid:
print("Uploads failed validation")
exit(2)
if not args.quiet:
print("Uploaded files ok.")
cert_info = get_ipmi_cert_info(session, args.ipmi_url, model, args.username, args.password)
if not cert_info:
print("Failed to extract certificate information from IPMI!")
if model == "X11":
print("Try checking either your IPMI network config or your IPMI license!")
exit(2)
if not args.quiet and cert_info['has_cert']:
print("After upload, there exists a certificate, which is valid until: %s" % cert_info['valid_until'])
if not args.no_reboot:
if not args.quiet:
print("Rebooting IPMI to apply changes.")
if not reboot_ipmi(session, args.ipmi_url, model, args.username, args.password):
print("Rebooting failed! Go reboot it manually?")
if not args.quiet:
print("All done!")
if __name__ == "__main__":
main()
@Kevdome3000
Copy link

hey i saw your comment on NovaUI about your frustration with the source code obfuscation. I'm in the same boat, and it's driving me nuts. Ive been able to decode a good portion of it, and if your interested in collaborating to decode the rest, ping me on discord : kevdome3000#9311

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment