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:
commit
32cfa8fe5d
@ -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
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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()
|
||||
|
@ -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
106
pyghmi/storage.py
Normal 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
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user