2
0
mirror of https://opendev.org/x/pyghmi synced 2025-01-27 19:37:44 +00:00

Merge "Add storage configuration"

This commit is contained in:
Zuul 2017-11-28 20:05:03 +00:00 committed by Gerrit Code Review
commit 32cfa8fe5d
6 changed files with 492 additions and 0 deletions

View File

@ -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

View File

@ -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')

View File

@ -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()

View File

@ -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':

106
pyghmi/storage.py Normal file
View File

@ -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

View File

@ -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: