diff --git a/pyghmi/exceptions.py b/pyghmi/exceptions.py index 869f4443..a9e2b803 100644 --- a/pyghmi/exceptions.py +++ b/pyghmi/exceptions.py @@ -42,3 +42,9 @@ class InvalidParameterValue(PyghmiException): class BmcErrorException(IpmiException): # This denotes when library detects an invalid BMC behavior pass + + +class UnsupportedFunctionality(PyghmiException): + # Indicates when functionality is requested that is not supported by + # current endpoint + pass diff --git a/pyghmi/ipmi/command.py b/pyghmi/ipmi/command.py index 29df0546..fdbcc76f 100644 --- a/pyghmi/ipmi/command.py +++ b/pyghmi/ipmi/command.py @@ -1709,3 +1709,18 @@ class Command(object): """Get graphical console launcher""" self.oem_init() return self._oem.get_graphical_console() + + def attach_remote_media(self, url, username=None, password=None): + """Attach remote media by url + + Given a url, attach remote media (cd/usb image) to the target system. + + :param url: URL to indicate where to find image (protocol support + varies by BMC) + :param username: Username for endpoint to use when accessing the URL. + If applicable, 'domain' would be indicated by '@' or + '\' syntax. + :param password: Password for endpoint to use when accessing the URL. + """ + self.oem_init() + self._oem.attach_remote_media(url, username, password) diff --git a/pyghmi/ipmi/oem/generic.py b/pyghmi/ipmi/oem/generic.py index 2ee1690f..5d7e84eb 100644 --- a/pyghmi/ipmi/oem/generic.py +++ b/pyghmi/ipmi/oem/generic.py @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pyghmi.exceptions as exc + class OEMHandler(object): """Handler class for OEM capabilities. @@ -221,3 +223,6 @@ class OEMHandler(object): :param netdata: Dictionary to store additional network data """ return + + def attach_remote_media(self, imagename, username, password): + raise exc.UnsupportedFunctionality() diff --git a/pyghmi/ipmi/oem/lenovo/handler.py b/pyghmi/ipmi/oem/lenovo/handler.py index 735e398f..0949e2d3 100755 --- a/pyghmi/ipmi/oem/lenovo/handler.py +++ b/pyghmi/ipmi/oem/lenovo/handler.py @@ -40,6 +40,7 @@ from pyghmi.ipmi.oem.lenovo import raid_drive import pyghmi.util.webclient as wc import socket +import struct inventory.register_inventory_category(cpu) inventory.register_inventory_category(dimm) @@ -114,6 +115,12 @@ led_status = { led_status_default = "Blink" +def _megarac_abbrev_image(name): + if len(name) <= 16: + return name + return name[:16] + name[-4:] + + class OEMHandler(generic.OEMHandler): # noinspection PyUnusedLocal def __init__(self, oemid, ipmicmd): @@ -121,6 +128,7 @@ class OEMHandler(generic.OEMHandler): # variations. For example System X versus Thinkserver self.oemid = oemid self.ipmicmd = ipmicmd + self._has_megarac = None self.oem_inventory_info = None def get_video_launchdata(self): @@ -543,3 +551,128 @@ class OEMHandler(generic.OEMHandler): ipv6str = ':'.join([ipv6str[x:x+4] for x in xrange(0, 32, 4)]) netdata['ipv6_addresses'] = [ '{0}/{1}'.format(ipv6str, ipv6_prefix)] + + @property + def has_megarac(self): + # if there is functionality that is the same for tsm or generic + # megarac, then this is appropriate. If there's a TSM specific + # preferred, use has_tsm first + if self._has_megarac is not None: + return self._has_megarac + self._has_megarac = False + try: + rsp = self.ipmicmd.xraw_command(netfn=0x32, command=0x7e) + # We don't have a handy classify-only, so use get sel policy + # rsp should have a length of one, and be either '\x00' or '\x01' + if len(rsp['data'][:]) == 1 and rsp['data'][0] in ('\x00', '\x01'): + self._has_megarac = True + except pygexc.IpmiException: + pass # Means that it's not going to be a megarac + return self._has_megarac + + def _set_short_ris_string(self, selector, value): + data = (1, selector, 0) + struct.unpack('{0}B'.format(len(value)), + value) + self.ipmicmd.xraw_command(netfn=0x32, command=0x9f, data=data) + + def _set_ris_string(self, selector, value): + if len(value) > 256: + raise pygexc.UnsupportedFunctionality( + 'Value exceeds 256 characters: {0}'.format(value)) + padded = value + (256 - len(value)) * '\x00' + padded = list(struct.unpack('256B', padded)) + # 8 = RIS, 4 = hd, 2 = fd, 1 = cd + try: # try and clear in-progress if left incomplete + self.ipmicmd.xraw_command(netfn=0x32, command=0x9f, + data=(1, selector, 0, 0)) + except pygexc.IpmiException: + pass + # set in-progress + self.ipmicmd.xraw_command(netfn=0x32, command=0x9f, + data=(1, selector, 0, 1)) + # now do the set + for x in xrange(0, 256, 64): + currdata = padded[x:x+64] + currchunk = x // 64 + 1 + cmddata = [1, selector, currchunk] + currdata + self.ipmicmd.xraw_command(netfn=0x32, command=0x9f, data=cmddata) + # unset in-progress + self.ipmicmd.xraw_command(netfn=0x32, command=0x9f, + data=(1, selector, 0, 0)) + + def _megarac_fetch_image_shortnames(self): + rsp = self.ipmicmd.xraw_command(netfn=0x32, command=0xd8, + data=(7, 1, 0)) + imgnames = rsp['data'][1:] + shortnames = [] + for idx in xrange(0, len(imgnames), 22): + shortnames.append(imgnames[idx+2:idx+22].rstrip('\0')) + return shortnames + + def _megarac_media_waitforready(self, imagename): + # first, we have, sadly, a 10 second grace period for some invisible + # async activity to get far enough long to monitor + self.ipmicmd.ipmi_session.pause(10) + risenabled = '\x00' + mountok = '\xff' + while risenabled != '\x01': + risenabled = self.ipmicmd.xraw_command( + netfn=0x32, command=0x9e, data=(8, 10))['data'][2] + while mountok == '\xff': + mountok = self.ipmicmd.xraw_command( + netfn=0x32, command=0x9e, data=(1, 8))['data'][2] + targshortname = _megarac_abbrev_image(imagename) + shortnames = self._megarac_fetch_image_shortnames() + while targshortname not in shortnames: + self.ipmicmd.wait_for_rsp(1) + shortnames = self._megarac_fetch_image_shortnames() + self.ipmicmd.ipmi_session.pause(10) + try: + self.ipmicmd.xraw_command(netfn=0x32, command=0xa0, data=(1, 0)) + self.ipmicmd.ipmi_session.pause(5) + except pygexc.IpmiException: + pass + + def _megarac_attach_media(self, proto, username, password, imagename, + domain, path, host): + # First we must ensure that the RIS is actually enabled + self.ipmicmd.xraw_command(netfn=0x32, command=0x9f, data=(8, 10, 0, 1)) + if username is not None: + self._set_ris_string(3, username) + if password is not None: + self._set_short_ris_string(4, password) + if domain is not None: + self._set_ris_string(6, domain) + self._set_ris_string(1, path) + ip = util.get_ipv4(host)[0] + self._set_short_ris_string(2, ip) + self._set_short_ris_string(5, proto) + # now to restart RIS to have changes take effect... + self.ipmicmd.xraw_command(netfn=0x32, command=0x9f, data=(8, 11)) + # now to kick off the requested mount + self._megarac_media_waitforready(imagename) + self._set_ris_string(0, imagename) + self.ipmicmd.xraw_command(netfn=0x32, command=0xa0, + data=(1, 1)) + + def attach_remote_media(self, url, username, password): + if self.has_megarac: + proto, host, path = util.urlsplit(url) + if proto == 'smb': + proto = 'cifs' + domain = None + path, imagename = path.rsplit('/', 1) + if username is not None and '@' in username: + username, domain = username.split('@', 1) + elif username is not None and '\\' in username: + domain, username = username.split('\\', 1) + try: + self._megarac_attach_media(proto, username, password, + imagename, domain, path, host) + except pygexc.IpmiException as ie: + if ie.ipmicode in (0x92, 0x99): + # if starting from scratch, this can happen... + self._megarac_attach_media(proto, username, password, + imagename, domain, path, host) + else: + raise diff --git a/pyghmi/ipmi/oem/lookup.py b/pyghmi/ipmi/oem/lookup.py index e123470b..b632bbe2 100755 --- a/pyghmi/ipmi/oem/lookup.py +++ b/pyghmi/ipmi/oem/lookup.py @@ -21,6 +21,8 @@ import pyghmi.ipmi.oem.lenovo.handler as lenovo oemmap = { 20301: lenovo, # IBM x86 (and System X at Lenovo) 19046: lenovo, # Lenovo x86 (e.g. Thinkserver) + 7154: lenovo, # Technically, standard IPMI, but give lenovo a chance + # to check for MegaRAC } diff --git a/pyghmi/ipmi/private/session.py b/pyghmi/ipmi/private/session.py index 5fd24ca4..533c61a8 100644 --- a/pyghmi/ipmi/private/session.py +++ b/pyghmi/ipmi/private/session.py @@ -678,7 +678,7 @@ class Session(object): raise exc.IpmiException('Session no longer connected') return lastresponse - def _send_ipmi_net_payload(self, netfn=None, command=None, data=[], code=0, + def _send_ipmi_net_payload(self, netfn=None, command=None, data=(), code=0, bridge_request=None, retry=None, delay_xmit=None, timeout=None): if retry is None: @@ -966,6 +966,12 @@ class Session(object): self._initsession() self._get_channel_auth_cap() + @classmethod + def pause(cls, timeout): + starttime = _monotonic_time() + while _monotonic_time() - starttime < timeout: + cls.wait_for_rsp(timeout - (_monotonic_time() - starttime)) + @classmethod def wait_for_rsp(cls, timeout=None, callout=True): """IPMI Session Event loop iteration diff --git a/pyghmi/ipmi/private/util.py b/pyghmi/ipmi/private/util.py index 40afcda3..daec6647 100644 --- a/pyghmi/ipmi/private/util.py +++ b/pyghmi/ipmi/private/util.py @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import socket import struct @@ -29,3 +30,28 @@ def decode_wireformat_uuid(rawguid): bebytes = struct.unpack_from('>HHI', buffer(rawguid[8:])) return '{0:04X}-{1:02X}-{2:02X}-{3:02X}-{4:02X}{5:04X}'.format( lebytes[0], lebytes[1], lebytes[2], bebytes[0], bebytes[1], bebytes[2]) + + +def urlsplit(url): + """Split an arbitrary url into protocol, host, rest + + The standard urlsplit does not want to provide 'netloc' for arbitrary + protocols, this works around that. + + :param url: The url to split into component parts + """ + proto, rest = url.split(':', 1) + host = '' + if rest[:2] == '//': + host, rest = rest[2:].split('/', 1) + rest = '/' + rest + return proto, host, rest + + +def get_ipv4(hostname): + """Get list of ipv4 addresses for hostname + + """ + addrinfo = socket.getaddrinfo(hostname, None, socket.AF_INET, + socket.SOCK_STREAM) + return [addrinfo[x][4][0] for x in xrange(len(addrinfo))]