diff --git a/pyghmi/ipmi/command.py b/pyghmi/ipmi/command.py index 14ca1712..cb350085 100644 --- a/pyghmi/ipmi/command.py +++ b/pyghmi/ipmi/command.py @@ -1765,6 +1765,20 @@ class Command(object): self.oem_init() return self._oem.get_graphical_console() + def update_firmware(self, file, data=None, progress=None): + """Send file to BMC to perform firmware update + + :param filename: The filename to upload to the target BMC + :param data: The payload of the firmware. Default is to read from + specified filename. + :param progress: A callback that will be given a dict describing + update process. Provide if + """ + self.oem_init() + if progress is None: + progress = lambda x: True + self._oem.update_firmware(file, data, progress) + def attach_remote_media(self, url, username=None, password=None): """Attach remote media by url diff --git a/pyghmi/ipmi/oem/generic.py b/pyghmi/ipmi/oem/generic.py index 6925641d..4a1c4173 100644 --- a/pyghmi/ipmi/oem/generic.py +++ b/pyghmi/ipmi/oem/generic.py @@ -198,6 +198,10 @@ class OEMHandler(object): """ return () + def update_firmware(self, filename, data=None, progress=None): + raise exc.UnsupportedFunctionality( + 'Firmware update not supported on this platform') + def get_graphical_console(self): """Get graphical console launcher""" return () diff --git a/pyghmi/ipmi/oem/lenovo/handler.py b/pyghmi/ipmi/oem/lenovo/handler.py index 96948265..a70f2f59 100755 --- a/pyghmi/ipmi/oem/lenovo/handler.py +++ b/pyghmi/ipmi/oem/lenovo/handler.py @@ -828,6 +828,13 @@ class OEMHandler(generic.OEMHandler): else: raise + def update_firmware(self, filename, data=None, progress=None): + if self.has_xcc: + return self.immhandler.update_firmware( + filename, data=data, progress=progress) + super(OEMHandler, self).update_firmware(filename, data=data, + progress=progress) + def detach_remote_media(self): if self.has_imm: self.immhandler.detach_remote_media() diff --git a/pyghmi/ipmi/oem/lenovo/imm.py b/pyghmi/ipmi/oem/lenovo/imm.py index 5cc54d0c..22ba3130 100644 --- a/pyghmi/ipmi/oem/lenovo/imm.py +++ b/pyghmi/ipmi/oem/lenovo/imm.py @@ -16,12 +16,28 @@ from datetime import datetime import json +import pyghmi.ipmi.private.session as ipmisession import pyghmi.ipmi.private.util as util import pyghmi.util.webclient as webclient +import random +import threading import urllib import weakref +class FileUploader(threading.Thread): + + def __init__(self, webclient, url, filename, data): + self.wc = webclient + self.url = url + self.filename = filename + self.data = data + super(FileUploader, self).__init__() + + def run(self): + self.rsp = self.wc.upload(self.url, self.filename, self.data) + + class IMMClient(object): logouturl = '/data/logout' bmcname = 'IMM' @@ -480,3 +496,95 @@ class XCCClient(IMMClient): json.dumps({'Slot': slot})) if 'return' not in rt or rt['return'] != 0: raise Exception("Unrecognized return: " + repr(rt)) + + def update_firmware(self, filename, data=None, progress=None): + try: + self.update_firmware_backend(filename, data, progress) + except Exception: + self.wc.grab_json_response('/api/providers/fwupdate', json.dumps( + {'UPD_WebCancel': 1})) + raise + + def update_firmware_backend(self, filename, data=None, progress=None): + rsv = self.wc.grab_json_response('/api/providers/fwupdate', json.dumps( + {'UPD_WebReserve': 1})) + if rsv['return'] != 0: + raise Exception('Unexpected return to reservation: ' + repr(rsv)) + xid = random.randint(0, 1000000000) + uploadthread = FileUploader(self.wc.dupe(), + '/upload?X-Progress-ID={0}'.format(xid), + filename, data) + uploadthread.start() + uploadstate = None + while uploadthread.isAlive(): + uploadthread.join(3) + rsp = self.wc.grab_json_response( + '/upload/progress?X-Progress-ID={0}'.format(xid)) + if rsp['state'] == 'uploading': + progress({'phase': 'upload', + 'progress': 100.0 * rsp['received'] / rsp['size']}) + elif rsp['state'] != 'done': + raise Exception('Unexpected result:' + repr(rsp)) + uploadstate = rsp['state'] + self.wc.grab_json_response('/api/providers/identity') + while uploadstate != 'done': + rsp = self.wc.grab_json_response( + '/upload/progress?X-Progress-ID={0}'.format(xid)) + uploadstate = rsp['state'] + self.wc.grab_json_response('/api/providers/identity') + rsp = json.loads(uploadthread.rsp) + if rsp['items'][0]['name'] != filename: + raise Exception('Unexpected response: ' + repr(rsp)) + progress({'phase': 'upload', + 'progress': 100.0}) + self.wc.grab_json_response('/api/providers/identity') + if '_csrf_token' in self.wc.cookies: + self.wc.set_header('X-XSRF-TOKEN', self.wc.cookies['_csrf_token']) + rsp = self.wc.grab_json_response('/api/providers/fwupdate', json.dumps( + {'UPD_WebSetFileName': rsp['items'][0]['path']})) + if rsp['return'] != 0: + raise Exception('Unexpected return to set filename: ' + repr(rsp)) + rsp = self.wc.grab_json_response('/api/providers/fwupdate', json.dumps( + {'UPD_WebVerifyUploadFile': 1})) + if rsp['return'] != 0: + raise Exception('Unexpected return to verify: ' + repr(rsp)) + self.wc.grab_json_response('/api/providers/identity') + rsp = self.wc.grab_json_response( + '/upload/progress?X-Progress-ID={0}'.format(xid)) + if rsp['state'] != 'done': + raise Exception('Unexpected progress: ' + repr(rsp)) + rsp = self.wc.grab_json_response('/api/dataset/imm_firmware_success') + if len(rsp['items']) != 1: + raise Exception('Unexpected result: ' + repr(rsp)) + rsp = self.wc.grab_json_response('/api/dataset/imm_firmware_update') + if rsp['items'][0]['upgrades'][0]['id'] != 1: + raise Exception('Unexpected answer: ' + repr(rsp)) + if '_csrf_token' in self.wc.cookies: + self.wc.set_header('X-XSRF-TOKEN', self.wc.cookies['_csrf_token']) + rsp = self.wc.grab_json_response('/api/providers/fwupdate', json.dumps( + {'UPD_WebStartDefaultAction': 1})) + if rsp['return'] != 0: + raise Exception('Unexpected result starting update: ' + + rsp['return']) + complete = False + while not complete: + ipmisession.Session.pause(3) + rsp = self.wc.grab_json_response( + '/api/dataset/imm_firmware_progress') + progress({'phase': 'apply', + 'progress': rsp['items'][0]['action_percent_complete']}) + if rsp['items'][0]['action_state'] == 'Idle': + complete = True + break + if rsp['items'][0]['action_state'] == 'Complete OK': + complete = True + if rsp['items'][0]['action_status'] != 0: + raise Exception('Unexpected failure: ' + repr(rsp)) + break + if (rsp['items'][0]['action_state'] == 'In Progress' and + rsp['items'][0]['action_status'] == 2): + raise Exception('Unexpected failure: ' + repr(rsp)) + if rsp['items'][0]['action_state'] != 'In Progress': + raise Exception( + 'Unknown condition waiting for ' + 'firmware update: ' + repr(rsp)) diff --git a/pyghmi/ipmi/private/session.py b/pyghmi/ipmi/private/session.py index 9544bb3d..3ed65a60 100644 --- a/pyghmi/ipmi/private/session.py +++ b/pyghmi/ipmi/private/session.py @@ -547,7 +547,7 @@ class Session(object): myport = self.socket.getsockname()[1] for sockaddr in self.allsockaddrs: if (sockaddr in Session.bmc_handlers and - myport in Session.bmc_hansdlers[sockaddr]): + myport in Session.bmc_handlers[sockaddr]): del Session.bmc_handlers[sockaddr][myport] if Session.bmc_handlers[sockaddr] == {}: del Session.bmc_handlers[sockaddr] diff --git a/pyghmi/util/webclient.py b/pyghmi/util/webclient.py index ce9bedec..9bda6917 100644 --- a/pyghmi/util/webclient.py +++ b/pyghmi/util/webclient.py @@ -31,18 +31,49 @@ except ImportError: __author__ = 'jjohnson2' +# Used as the separator for form data +BND = 'TbqbLUSn0QFjx9gxiQLtgBK4Zu6ehLqtLs4JOBS50EgxXJ2yoRMhTrmRXxO1lkoAQdZx16' + +# We will frequently be dealing with the same data across many instances, +# consolidate forms to single memory location to get benefits.. +uploadforms = {} + + +def get_upload_form(filename, data): + try: + return uploadforms[filename] + except KeyError: + form = '--' + BND + '\r\nContent-Disposition: form-data; ' \ + 'name="{0}"; filename="{0}"\r\n'.format(filename) + form += 'Content-Type: application/octet-stream\r\n\r\n' + data + form += '\r\n--' + BND + '--\r\n' + uploadforms[filename] = form + return form + + class SecureHTTPConnection(httplib.HTTPConnection, object): default_port = httplib.HTTPS_PORT def __init__(self, host, port=None, key_file=None, cert_file=None, - ca_certs=None, strict=None, verifycallback=None, **kwargs): + ca_certs=None, strict=None, verifycallback=None, clone=None, + **kwargs): if 'timeout' not in kwargs: kwargs['timeout'] = 60 + self.thehost = host + self.theport = port httplib.HTTPConnection.__init__(self, host, port, strict, **kwargs) self.cert_reqs = ssl.CERT_NONE # verification will be done ssh style.. - self._certverify = verifycallback - self.cookies = {} - self.stdheaders = {} + if clone: + self._certverify = clone._certverify + self.cookies = clone.cookies.copy() + self.stdheaders = clone.stdheaders.copy() + else: + self._certverify = verifycallback + self.cookies = {} + self.stdheaders = {} + + def dupe(self): + return SecureHTTPConnection(self.thehost, self.theport, clone=self) def set_header(self, key, value): self.stdheaders[key] = value @@ -76,6 +107,33 @@ class SecureHTTPConnection(httplib.HTTPConnection, object): rsp.read() return {} + def upload(self, url, filename, data=None): + """Upload a file to the url + + :param url: + :param filename: The name of the file + :param data: A file object or data to use rather than reading from + the file. + :return: + """ + if data is None: + data = open(filename, 'rb') + if isinstance(data, file): + data = data.read() + form = get_upload_form(filename, data) + ulheaders = self.stdheaders.copy() + ulheaders['Content-Type'] = 'multipart/form-data; boundary=' + BND + self.request('POST', url, form, ulheaders) + rsp = self.getresponse() + # peer updates in progress should already have pointers, + # subsequent transactions will cause memory to needlessly double, + # but easiest way to keep memory relatively low + del uploadforms[filename] + if rsp.status != 200: + raise Exception('Unexpected response in file upload: ' + + rsp.read()) + return rsp.read() + def request(self, method, url, body=None, headers=None): if headers is None: headers = self.stdheaders.copy()