diff --git a/pyghmi/ipmi/command.py b/pyghmi/ipmi/command.py index 9cd0b946..1def11fe 100644 --- a/pyghmi/ipmi/command.py +++ b/pyghmi/ipmi/command.py @@ -755,6 +755,59 @@ class Command(object): cmddata = bytearray((channel, 12)) + socket.inet_aton(ipv4_gateway) self.xraw_command(netfn=0xc, command=1, data=cmddata) + def get_storage_configuration(self): + """"Get storage configuration data + + Retrieves the storage configuration from the target. Data is given + about disks, pools, and volumes. When referencing something, use the + relevant 'cfgpath' attribute to describe it. It is not guaranteed that + cfgpath will be consistent version to version, so a lookup is suggested + in end user applications. + + :return: A pyghmi.storage.ConfigSpec object describing current config + """ + self.oem_init() + return self._oem.get_storage_configuration() + + def clear_storage_arrays(self): + """Remove all array and dependent volumes from the system + + :return: + """ + self.oem_init() + self._oem.clear_storage_arrays() + + def remove_storage_configuration(self, cfgspec): + """Remove specified storage configuration from controller. + + :param cfgspec: A pyghmi.storage.ConfigSpec describing what to remove + :return: + """ + self.oem_init() + return self._oem.remove_storage_configuration(cfgspec) + + def apply_storage_configuration(self, cfgspec=None): + """Evaluate a configuration for validity + + This will check if configuration is currently available and, if given, + whether the specified cfgspec can be applied. + :param cfgspec: A pyghmi.storage.ConfigSpec describing desired oonfig + :return: + """ + self.oem_init() + return self._oem.apply_storage_configuration(cfgspec) + + def check_storage_configuration(self, cfgspec=None): + """Evaluate a configuration for validity + + This will check if configuration is currently available and, if given, + whether the specified cfgspec can be applied. + :param cfgspec: A pyghmi.storage.ConfigSpec describing desired oonfig + :return: + """ + self.oem_init() + return self._oem.check_storage_configuration(cfgspec) + def get_net_configuration(self, channel=None, gateway_macs=True): """Get network configuration data diff --git a/pyghmi/ipmi/oem/generic.py b/pyghmi/ipmi/oem/generic.py index 62f925ae..d3294b0f 100644 --- a/pyghmi/ipmi/oem/generic.py +++ b/pyghmi/ipmi/oem/generic.py @@ -198,6 +198,26 @@ class OEMHandler(object): """ return () + def clear_storage_arrays(self): + raise exc.UnsupportedFunctionality( + 'Remote storage configuration not supported on this platform') + + def remove_storage_configuration(self, cfgspec): + raise exc.UnsupportedFunctionality( + 'Remote storage configuration not supported on this platform') + + def apply_storage_configuration(self, cfgspec): + raise exc.UnsupportedFunctionality( + 'Remote storage configuration not supported on this platform') + + def check_storage_configuration(self, cfgspec): + raise exc.UnsupportedFunctionality( + 'Remote storage configuration not supported on this platform') + + def get_storage_configuration(self): + raise exc.UnsupportedFunctionality( + 'Remote storage configuration not supported on this platform') + def update_firmware(self, filename, data=None, progress=None, bank=None): raise exc.UnsupportedFunctionality( 'Firmware update not supported on this platform') diff --git a/pyghmi/ipmi/oem/lenovo/handler.py b/pyghmi/ipmi/oem/lenovo/handler.py index ae5d8514..99e73ef8 100755 --- a/pyghmi/ipmi/oem/lenovo/handler.py +++ b/pyghmi/ipmi/oem/lenovo/handler.py @@ -165,6 +165,31 @@ class OEMHandler(generic.OEMHandler): self._mrethidx = rsp['data'][0] return self._mrethidx + def remove_storage_configuration(self, cfgspec): + if self.has_xcc: + return self.immhandler.remove_storage_configuration(cfgspec) + return super(OEMHandler, self).remove_storage_configuration() + + def clear_storage_arrays(self): + if self.has_xcc: + return self.immhandler.clear_storage_arrays() + return super(OEMHandler, self).clear_storage_ararys() + + def apply_storage_configuration(self, cfgspec): + if self.has_xcc: + return self.immhandler.apply_storage_configuration(cfgspec) + return super(OEMHandler, self).apply_storage_configuration() + + def check_storage_configuration(self, cfgspec): + if self.has_xcc: + return self.immhandler.check_storage_configuration(cfgspec) + return super(OEMHandler, self).get_storage_configuration() + + def get_storage_configuration(self): + if self.has_xcc: + return self.immhandler.get_storage_configuration() + return super(OEMHandler, self).get_storage_configuration() + def get_video_launchdata(self): if self.has_tsm: return self.get_tsm_launchdata() diff --git a/pyghmi/ipmi/oem/lenovo/imm.py b/pyghmi/ipmi/oem/lenovo/imm.py index 033875a8..84e82404 100644 --- a/pyghmi/ipmi/oem/lenovo/imm.py +++ b/pyghmi/ipmi/oem/lenovo/imm.py @@ -24,6 +24,7 @@ import pyghmi.ipmi.oem.lenovo.energy as energy import pyghmi.ipmi.private.session as ipmisession import pyghmi.ipmi.private.util as util import pyghmi.ipmi.sdr as sdr +import pyghmi.storage as storage import pyghmi.util.webclient as webclient import random import socket @@ -485,6 +486,291 @@ class XCCClient(IMMClient): wc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token']) return wc + def _raid_number_map(self, controller): + themap = {} + rsp = self.wc.grab_json_response( + '/api/function/raid_conf?' + 'params=raidlink_GetDisksToConf,{0}'.format(controller)) + for lvl in rsp['items'][0]['supported_raidlvl']: + mapdata = (lvl['rdlvl'], lvl['maxSpan']) + raidname = lvl['rdlvlstr'].replace(' ', '').lower() + themap[raidname] = mapdata + raidname = raidname.replace('raid', 'r') + themap[raidname] = mapdata + raidname = raidname.replace('r', '') + themap[raidname] = mapdata + return themap + + def check_storage_configuration(self, cfgspec=None): + rsp = self.wc.grab_json_response( + '/api/function/raid_conf?params=raidlink_GetStatus') + if rsp['items'][0]['status'] != 2: + raise pygexc.TemporaryError('Storage configuration unavilable in ' + 'current state (try boot to setup or ' + 'an OS)') + if not cfgspec: + return True + for pool in cfgspec.arrays: + self._parse_storage_cfgspec(pool) + return True + + def _parse_array_spec(self, arrayspec): + controller = None + if arrayspec.disks: + for disk in list(arrayspec.disks) + list(arrayspec.hotspares): + if controller is None: + controller = disk.id[0] + if controller != disk.id[0]: + raise pygexc.UnsupportedFunctionality( + 'Cannot span arrays across controllers') + raidmap = self._raid_number_map(controller) + if not raidmap: + raise pygexc.InvalidParameterValue( + 'There are no available drives for a new array') + requestedlevel = str(arrayspec.raid).lower() + if requestedlevel not in raidmap: + raise pygexc.InvalidParameterValue( + 'Requested RAID "{0}" not available on this ' + 'system with currently available drives'.format( + requestedlevel)) + rdinfo = raidmap[str(arrayspec.raid).lower()] + rdlvl = str(rdinfo[0]) + defspan = 1 if rdinfo[1] == 1 else 2 + spancount = defspan if arrayspec.spans is None else arrayspec.spans + drivesperspan = str(len(arrayspec.disks) // int(spancount)) + hotspares = arrayspec.hotspares + drives = arrayspec.disks + if hotspares: + hstr = '|'.join([str(x.id[1]) for x in hotspares]) + '|' + else: + hstr = '' + drvstr = '|'.join([str(x.id[1]) for x in drives]) + '|' + pth = '/api/function/raid_conf?params=raidlink_CheckConfisValid' + args = [pth, controller, rdlvl, spancount, drivesperspan, drvstr, + hstr] + url = ','.join([str(x) for x in args]) + rsp = self.wc.grab_json_response(url) + if rsp['items'][0]['errcode'] == 16: + raise pygexc.InvalidParameterValue('Incorrect number of disks') + elif rsp['items'][0]['errcode'] != 0: + raise pygexc.InvalidParameterValue( + 'Invalid configuration: {0}'.format( + rsp['items'][0]['errcode'])) + return { + 'capacity': rsp['items'][0]['freeCapacity'], + 'controller': controller, + 'drives': drvstr, + 'hotspares': hstr, + 'raidlevel': rdlvl, + 'spans': spancount, + 'perspan': drivesperspan, + } + else: + pass # TODO: adding new volume to existing array would be here + + def _make_jbod(self, disk, realcfg): + currstatus = self._get_status(disk, realcfg) + if currstatus.lower() == 'jbod': + return + self._make_available(disk, realcfg) + self._set_drive_state(disk, 16) + + def _make_global_hotspare(self, disk, realcfg): + currstatus = self._get_status(disk, realcfg) + if currstatus.lower() == 'global hot spare': + return + self._make_available(disk, realcfg) + self._set_drive_state(disk, 1) + + def _make_available(self, disk, realcfg): + # 8 if jbod, 4 if hotspare.., leave alone if already... + currstatus = self._get_status(disk, realcfg) + newstate = None + if currstatus == 'Unconfigured Good': + return + elif currstatus.lower() == 'global hot spare': + newstate = 4 + elif currstatus.lower() == 'jbod': + newstate = 8 + self._set_drive_state(disk, newstate) + + def _get_status(self, disk, realcfg): + for cfgdisk in realcfg.disks: + if disk.id == cfgdisk.id: + currstatus = cfgdisk.status + break + else: + raise pygexc.InvalidParameterValue('Requested disk not found') + return currstatus + + def _set_drive_state(self, disk, state): + rsp = self.wc.grab_json_response( + '/api/function', + {'raidlink_DiskStateAction': '{0},{1}'.format(disk.id[1], state)}) + if rsp['return'] != 0: + raise Exception( + 'Unexpected return to set disk state: {0}'.format( + rsp['return'])) + + def clear_storage_arrays(self): + rsp = self.wc.grab_json_response( + '/api/function', {'raidlink_ClearRaidConf': '1'}) + if rsp['return'] != 0: + raise Exception('Unexpected return to clear config: ' + repr(rsp)) + + def remove_storage_configuration(self, cfgspec): + realcfg = self.get_storage_configuration() + for pool in cfgspec.arrays: + for volume in pool.volumes: + vid = str(volume.id[1]) + rsp = self.wc.grab_json_response( + '/api/function', {'raidlink_RemoveVolumeAsync': vid}) + if rsp['return'] != 0: + raise Exception( + 'Unexpected return to volume deletion: ' + repr(rsp)) + self._wait_storage_async() + for disk in cfgspec.disks: + self._make_available(disk, realcfg) + + def apply_storage_configuration(self, cfgspec): + realcfg = self.get_storage_configuration() + for disk in cfgspec.disks: + if disk.status.lower() == 'jbod': + self._make_jbod(disk, realcfg) + elif disk.status.lower() == 'hotspare': + self._make_global_hotspare(disk, realcfg) + elif disk.status.lower() in ('unconfigured', 'available', 'ugood', + 'unconfigured good'): + self._make_available(disk, realcfg) + for pool in cfgspec.arrays: + if pool.disks: + self._create_array(pool) + + def _create_array(self, pool): + params = self._parse_array_spec(pool) + url = '/api/function/raid_conf?params=raidlink_GetDefaultVolProp' + args = (url, params['controller'], 0, params['drives']) + props = self.wc.grab_json_response(','.join([str(x) for x in args])) + props = props['items'][0] + volumes = pool.volumes + remainingcap = params['capacity'] + nameappend = 1 + vols = [] + currvolnames = None + currcfg = None + for vol in volumes: + if vol.name is None: + # need to iterate while there exists a volume of that name + if currvolnames is None: + currcfg = self.get_storage_configuration() + currvolnames = set([]) + for pool in currcfg.arrays: + for volume in pool.volumes: + currvolnames.add(volume.name) + name = props['name'] + '_{0}'.format(nameappend) + nameappend += 1 + while name in currvolnames: + name = props['name'] + '_{0}'.format(nameappend) + nameappend += 1 + else: + name = vol.name + stripesize = props['stripsize'] if vol.stripesize is None \ + else vol.stripesize + strsize = 'remainder' if vol.size is None else str(vol.size) + if strsize in ('all', '100%'): + volsize = params['capacity'] + elif strsize in ('remainder', 'rest'): + volsize = remainingcap + elif strsize.endswith('%'): + volsize = int(params['capacity'] * + float(strsize.replace('%', '')) / 100.0) + else: + try: + volsize = int(strsize) + except ValueError: + raise pygexc.InvalidParameterValue( + 'Unrecognized size ' + strsize) + remainingcap -= volsize + if remainingcap < 0: + raise pygexc.InvalidParameterValue( + 'Requested sizes exceed available capacity') + vols.append('{0};{1};{2};{3};{4};{5};{6};{7};{8};|'.format( + name, volsize, stripesize, props['cpwb'], props['cpra'], + props['cpio'], props['ap'], props['dcp'], props['initstate'])) + url = '/api/function' + arglist = '{0},{1},{2},{3},{4},{5},'.format( + params['controller'], params['raidlevel'], params['spans'], + params['perspan'], params['drives'], params['hotspares']) + arglist += ''.join(vols) + parms = {'raidlink_AddNewVolWithNaAsync': arglist} + rsp = self.wc.grab_json_response(url, parms) + if rsp['return'] != 0: + raise Exception( + 'Unexpected response to add volume command: ' + repr(rsp)) + self._wait_storage_async() + + def _wait_storage_async(self): + rsp = {'items': [{'status': 0}]} + while rsp['items'][0]['status'] == 0: + ipmisession.Session.pause(1) + rsp = self.wc.grab_json_response( + '/api/function/raid_conf?params=raidlink_QueryAsyncStatus') + + def extract_drivelist(self, cfgspec, controller, drives): + for drive in cfgspec['drives']: + ctl, drive = self._extract_drive_desc(drive) + if controller is None: + controller = ctl + if ctl != controller: + raise pygexc.UnsupportedFunctionality( + 'Cannot span arrays across controllers') + drives.append(drive) + return controller + + def get_storage_configuration(self): + rsp = self.wc.grab_json_response( + '/api/function/raid_alldevices?params=storage_GetAllDevices') + standalonedisks = [] + pools = [] + for item in rsp['items']: + for cinfo in item['controllerInfo']: + cid = cinfo['id'] + for pool in cinfo['pools']: + volumes = [] + disks = [] + spares = [] + for volume in pool['volumes']: + volumes.append( + storage.Volume(name=volume['name'], + size=volume['capacity'], + status=volume['statusStr'], + id=(cid, volume['id']))) + for disk in pool['disks']: + diskinfo = storage.Disk( + name=disk['name'], description=disk['type'], + id=(cid, disk['id']), status=disk['RAIDState'], + serial=disk['serialNo'], fru=disk['fruPartNo']) + if disk['RAIDState'] == 'Dedicated Hot Spare': + spares.append(diskinfo) + else: + disks.append(diskinfo) + totalsize = pool['totalCapacityStr'].replace('GB', '') + totalsize = int(float(totalsize) * 1024) + freesize = pool['freeCapacityStr'].replace('GB', '') + freesize = int(float(freesize) * 1024) + pools.append(storage.Array( + disks=disks, raid=pool['rdlvlstr'], volumes=volumes, + id=(cid, pool['id']), hotspares=spares, + capacity=totalsize, available_capacity=freesize)) + for disk in cinfo['unconfiguredDisks']: + # can be unused, global hot spare, or JBOD + standalonedisks.append( + storage.Disk( + name=disk['name'], description=disk['type'], + id=(cid, disk['id']), status=disk['RAIDState'], + serial=disk['serialNo'], fru=disk['fruPartNo'])) + return storage.ConfigSpec(disks=standalonedisks, arrays=pools) + def attach_remote_media(self, url, user, password): proto, host, path = util.urlsplit(url) if proto == 'smb': diff --git a/pyghmi/storage.py b/pyghmi/storage.py new file mode 100644 index 00000000..64e95b2a --- /dev/null +++ b/pyghmi/storage.py @@ -0,0 +1,106 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2017 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Disk(object): + def __init__(self, name, description=None, id=None, status=None, + serial=None, fru=None, stripesize=None): + """ + + :param name: A name descripbing the disk in human readable terms + :param description: A description of the device + :param id: Identifier used by the controller + :param status: Controller indicated status of disk + :param serial: Serial number of the drive + :param fru: FRU number of the driver + """ + self.name = str(name) + self.description = description + self.id = id + self.status = status + self.serial = serial + self.fru = fru + self.stripesize = stripesize + + +class Array(object): + def __init__(self, disks=None, raid=None, status=None, volumes=(), id=None, + spans=None, hotspares=(), capacity=None, + available_capacity=None): + """ + + :param disks: An array of Disk objects + :param layout: The layout of the Array, generally the RAID level + :param status: Status of the array according to the controller + :param id: Unique identifier used by controller to identify + :param spans: Number of spans for a multi-dimensional array + :param hotspares: List of Disk objects that are dedicated hot spares + for this array. + """ + self.disks = disks + self.raid = raid + self.status = status + self.id = id + self.volumes = volumes + self.spans = spans + self.hotspares = hotspares + self.capacity = capacity + self.available_capacity = available_capacity + + +class Volume(object): + def __init__(self, name=None, size=None, status=None, id=None, + stripesize=None): + """ + + :param name: Name of the volume + :param size: Size of the volume in MB + :param status: Controller indicated status of the volume + :param id: Controller idintefier of a given volume + :param stripesize: The stripesize of the volume + """ + self.name = name + if isinstance(size, int): + self.size = size + else: + strsize = str(size).lower() + if strsize.endswith('mb'): + self.size = int(strsize.replace('mb', '')) + elif strsize.endswith('gb'): + self.size = int(strsize.replace('gb', '')) * 1000 + elif strsize.endswith('tb'): + self.size = int(strsize.replace('tb', '')) * 1000 * 1000 + else: + self.size = size + self.status = status + self.id = id + self.stripesize = stripesize + + +class ConfigSpec(object): + def __init__(self, disks=(), arrays=()): + """A configuration specification of storage + + When returned from a remote system, it describes the current config. + When given to a remote system, it should only describe the delta + between current config. + + :param disks: A list of Disk in the configuration not in an array + :param arrays: A list of Array objects + :return: + """ + self.disks = disks + self.arrays = arrays diff --git a/pyghmi/util/webclient.py b/pyghmi/util/webclient.py index fa42e0b7..852d60e0 100644 --- a/pyghmi/util/webclient.py +++ b/pyghmi/util/webclient.py @@ -114,6 +114,8 @@ class SecureHTTPConnection(httplib.HTTPConnection, object): def grab_json_response(self, url, data=None, referer=None): webclient = self.dupe() + if isinstance(data, dict): + data = json.dumps(data) if data: webclient.request('POST', url, data, referer=referer) else: