# Copyright 2025 Lenovo Corporation
#
# 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.
import copy
import json
import re
import pyghmi.constants as pygconst
import pyghmi.redfish.oem.generic as generic
import pyghmi.exceptions as pygexc
import pyghmi.util.webclient as webclient
import pyghmi.storage as storage
import pyghmi.ipmi.private.util as util
import zipfile
import time
import os.path

numregex = re.compile('([0-9]+)')

def naturalize_string(key):
    """Analyzes string in a human way to enable natural sort

    :param key: string for the split
    :returns: A structure that can be consumed by 'sorted'
    """
    return [int(text) if text.isdigit() else text.lower()
            for text in re.split(numregex, key)]

def natural_sort(iterable):
    """Return a sort using natural sort if possible

    :param iterable:
    :return:
    """
    try:
        return sorted(iterable, key=naturalize_string)
    except TypeError:
        # The natural sort attempt failed, fallback to ascii sort
        return sorted(iterable)

class SensorReading(object):
    def __init__(self, healthinfo, sensor=None, value=None, units=None,
                 unavailable=False):
        if sensor:
            self.name = sensor['name']
        else:
            self.name = healthinfo['name']
            self.health = healthinfo['health']
            self.states = healthinfo['states']
            self.state_ids = healthinfo.get('state_ids', None)
        self.value = value
        self.imprecision = None
        self.units = units
        self.unavailable = unavailable

class OEMHandler(generic.OEMHandler):

    def __init__(self, sysinfo, sysurl, webclient, cache, gpool=None):
        super(OEMHandler, self).__init__(sysinfo, sysurl, webclient, cache,
                                         gpool)
        self.datacache = {}


    def supports_expand(self, url):
        return True
    
    def weblogout(self):
        if self.webclient:
            try:
                self.webclient.grab_json_response('/logout')
            except Exception:
                pass
    
    def get_cached_data(self, attribute, age=30):
        try:
            kv = self.datacache[attribute]
            if kv[1] > util._monotonic_time() - age:
                return kv[0]
        except KeyError:
            return None
    
    def get_inventory(self, withids=False):
        sysinfo = {
            'UUID': self._varsysinfo.get('UUID', '').lower(),
            'Serial Number': self._varsysinfo.get('SerialNumber', ''),
            'Manufacturer': self._varsysinfo.get('Manufacturer', ''),
            'Product name': self._varsysinfo.get('Model', ''),
            'Model': self._varsysinfo.get(
                'SKU', self._varsysinfo.get('PartNumber', '')),
        }
        yield ('System', sysinfo)
        for cpuinv in self._get_cpu_inventory():
            yield cpuinv
        for meminv in self._get_mem_inventory():
            yield meminv
        hwmap = self.hardware_inventory_map()
        for key in natural_sort(hwmap):
            yield (key, hwmap[key])
    
    def hardware_inventory_map(self):
        hwmap = self.get_cached_data('lenovo_cached_hwmap')
        if hwmap:
            return hwmap
        hwmap = {}
        for disk in self.disk_inventory(mode=1):  # hardware mode
            hwmap[disk[0]] = disk[1]
        adapterdata = self.get_cached_data('lenovo_cached_adapters')
        if not adapterdata:
            # if self.updating:
            #     raise pygexc.TemporaryError(
            #         'Cannot read extended inventory during firmware update')
            if self.webclient:
                adapterdata = list(self._do_bulk_requests([i['@odata.id'] for i in self.webclient.grab_json_response(
                        '/redfish/v1/Chassis/1')['Links']['PCIeDevices']]))
                if adapterdata:
                    self.datacache['lenovo_cached_adapters'] = (
                        adapterdata, util._monotonic_time())
        if adapterdata:
            anames = {}
            for adata, _ in adapterdata:
                skipadapter = False
                clabel = adata['Slot']['Location']['PartLocation'].get('LocationType','')
                if not clabel:
                    clabel = adata['Slot']['Location']['PartLocation'].get('ServiceLabel', '').split("=")[0]
                if clabel != 'Embedded':
                    aslot = adata['Slot']['Location']['PartLocation']['LocationOrdinalValue']
                    clabel = 'Slot {0}'.format(aslot)
                aname = adata['Name']
                bdata = {'location': clabel, 'name': aname}
                if aname in anames:
                    anames[aname] += 1
                    aname = '{0} {1}'.format(aname, anames[aname])
                else:
                    anames[aname] = 1
                pcislot = adata['Id'].split('_')[1]
                bdata['pcislot'] = '{0}:{1}.{2}'.format(
                    pcislot[:4].replace('0x',''), pcislot[4:6], pcislot[6:8]
                )
                serialdata = adata.get('SerialNumber', '')
                if (serialdata and serialdata != 'N/A'
                        and '---' not in serialdata):
                    bdata['serial'] = serialdata
                partnum = adata.get('PartNumber', '')
                if partnum and partnum != 'N/A':
                    bdata['Part Number'] = partnum
                fundata = self._get_expanded_data(adata['PCIeFunctions']['@odata.id'])
                venid = fundata['Members'][0].get('VendorId', None)
                if venid is not None:
                    bdata['PCI Vendor ID'] = venid.lower().split('0x')[-1]
                devid = fundata['Members'][0].get('DeviceId', None)
                if devid is not None and 'PCIE Device ID' not in bdata:
                    bdata['PCI Device ID'] = devid.lower().split('0x')[-1]
                subvenid = fundata['Members'][0].get('SubsystemVendorId', None)
                if subvenid is not None:
                    bdata['PCI Subsystem Vendor ID'] = subvenid.lower().split('0x')[-1]
                subdevid = fundata['Members'][0].get('SubsystemId', None)
                if subdevid is not None:
                    bdata['PCI Subsystem Device ID'] = subdevid.lower().split('0x')[-1]
                bdata['FRU Number'] = adata.get('SKU', '')

                # Could be identified also through Oem->Lenovo->FunctionClass
                if fundata['Members'][0]['DeviceClass'] == 'NetworkController':
                    ports_data = self._get_expanded_data('{0}/Ports'.format(adata['@odata.id'].replace('PCIeDevices','NetworkAdapters')))
                    macidx = 1
                    for port in ports_data['Members']:
                        if port.get('Ethernet', None):
                            macs = [x for x in port['Ethernet'].get('AssociatedMACAddresses', [])]
                            for mac in macs:
                                label = 'MAC Address {}'.format(macidx)
                                bdata[label] = generic._normalize_mac(mac)
                                macidx += 1
                        ibinfo = port.get('InfiniBand', {})
                        if ibinfo:
                            macs = [x for x in ibinfo.get('AssociatedPortGUIDs', [])]
                            for mac in macs:
                                label = 'Port GUID {}'.format(macidx)
                                bdata[label] = mac
                                macidx += 1
                hwmap[aname] = bdata
            self.datacache['lenovo_cached_hwmap'] = (hwmap,
                                                     util._monotonic_time())
        # self.weblogout()
        return hwmap

    def get_disk_firmware(self, diskent, prefix=''):
        bdata = {}
        if not prefix:
            location = diskent.get('Name', '')
            if location.startswith('M.2'):
                prefix = 'M.2-'
            elif location.startswith('7MM'):
                prefix = '7MM-'
        diskname = 'Disk {0}{1}'.format(prefix, diskent['PhysicalLocation']['PartLocation']['LocationOrdinalValue'])
        bdata['model'] = '{0}_{1}'.format(diskent['Manufacturer'].rstrip(), diskent['Model'].rstrip())
        bdata['version'] = diskent.get('FirmwareVersion','')
        return (diskname, bdata)

    def get_disk_hardware(self, diskent, prefix=''):
        bdata = {}
        if not prefix:
            location = diskent.get('Name', '')
            if location.startswith('M.2'):
                prefix = 'M.2-'
            elif location.startswith('7MM'):
                prefix = '7MM-'
        diskname = 'Disk {0}{1}'.format(prefix, diskent['PhysicalLocation']['PartLocation']['LocationOrdinalValue'])
        bdata['Model'] = '{0}_{1}'.format(diskent['Manufacturer'].rstrip(), diskent['Model'].rstrip())
        bdata['Serial Number'] = diskent['SerialNumber'].rstrip()
        bdata['FRU Number'] = diskent['SKU'].rstrip()
        bdata['Description'] = diskent['Oem']['Lenovo']['TypeString'].rstrip()
        return (diskname, bdata)

    def disk_inventory(self, mode=0):
        # mode 0 is firmware, 1 is hardware
        storagedata = self.get_cached_data('lenovo_cached_storage')
        if not storagedata:
                if self.webclient:
                    storagedata = self._do_bulk_requests([i['@odata.id'] for i in self.webclient.grab_json_response(
                        '/redfish/v1/Chassis/1')['Links']['Drives']])
                    if storagedata:
                        self.datacache['lenovo_cached_storage'] = (
                            storagedata, util._monotonic_time())
        # Unmanaged disks cannot be retrieved through Redfish API
        if storagedata:
            for diskent, _ in storagedata:
                if mode == 0:
                    yield self.get_disk_firmware(diskent)
                elif mode == 1:
                    yield self.get_disk_hardware(diskent)

    def get_storage_configuration(self, logout=True):
        rsp = self._get_expanded_data("/redfish/v1/Systems/1/Storage")
        standalonedisks = []
        pools = []
        for item in rsp.get('Members',[]):
            # Drives shown at 'Direct attached drives' in XCC
            # cannot be used for RAID creation
            if item['Id'].lower() == 'direct_attached_nvme':
                continue
            cdisks = [item['Drives'][i]['@odata.id'] for i in range(len(item['Drives']))]
            cid = '{0},{1}'.format(
                        item['Id'],
                        item['StorageControllers'][0]['Location']['PartLocation'].get('LocationOrdinalValue', -1))
            if item['Id'].lower() == 'vroc':
                cid = 'vroc,0'
            storage_pools = self._get_expanded_data(item['StoragePools']['@odata.id'])
            for p in storage_pools['Members']:
                vols = self._get_expanded_data(p['AllocatedVolumes']['@odata.id'])
                for vol in vols['Members']:
                    volumes=[]
                    disks=[]
                    spares=[]
                    volumes.append(
                        storage.Volume(name=vol['DisplayName'],
                                    size=int(vol['CapacityBytes'])/1024//1024,
                                    status=vol['Status']['Health'],
                                    id=(cid,vol['Id'])))
                    for item_key, disk_ids in vol['Links'].items():
                        if isinstance(disk_ids, list) and 'drives' in item_key.lower():
                            for disk in disk_ids:
                                if disk['@odata.id'] in cdisks:
                                    cdisks.remove(disk['@odata.id'])
                                disk_data = self.webclient.grab_json_response(disk['@odata.id'])
                                (spares if disk_data['Oem']['Lenovo']['DriveStatus']=="DedicatedHotspare" else disks).append(
                                    storage.Disk(
                                        name=disk_data['Name'], description=disk_data['Oem']['Lenovo']['TypeString'],
                                        id=(cid, disk_data['Id']), status=disk_data['Oem']['Lenovo']['DriveStatus'],
                                        serial=disk_data['SerialNumber'], fru=disk_data['SKU']))
                    raid=vol['RAIDType']
                totalsize = int(p['Capacity']['Data']['AllocatedBytes'])/1024//1024
                freesize = totalsize - int(p['Capacity']['Data']['ConsumedBytes'])/1024//1024
                pools.append(storage.Array(
                    disks=disks, raid=raid, volumes=volumes,
                    id=(cid, p['Id']), hotspares=spares,
                    capacity=totalsize, available_capacity=freesize))
            for d in cdisks:
                disk_data = self.webclient.grab_json_response(d)
                standalonedisks.append(
                    storage.Disk(
                        name=disk_data['Name'], description=disk_data['Oem']['Lenovo']['TypeString'],
                        id=(cid, disk_data['Id']), status=disk_data['Oem']['Lenovo']['DriveStatus'],
                        serial=disk_data['SerialNumber'], fru=disk_data['SKU']))
        return storage.ConfigSpec(disks=standalonedisks, arrays=pools)

    def check_storage_configuration(self, cfgspec=None):
        rsp = self.webclient.grab_json_response(
            '/api/providers/raidlink_GetStatus')
        if rsp['return'] != 0 or rsp['status'] != 1:
            raise pygexc.TemporaryError('Storage configuration unavailable in '
                                        'current state (try boot to setup or '
                                        'an OS)')
        return True

    def apply_storage_configuration(self, cfgspec):
        realcfg = self.get_storage_configuration(False)
        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 _make_available(self, disk, realcfg):
        currstatus = self._get_status(disk, realcfg)
        newstate = None
        if currstatus.lower() == 'unconfiguredgood':
            return
        elif currstatus.lower() == 'globalhotspare':
            newstate = "None"
        elif currstatus.lower() == 'jbod':
            newstate = "MakeUnconfiguredGood"
        self._set_drive_state(disk, newstate)

    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, "MakeJBOD")

    def _make_global_hotspare(self, disk, realcfg):
        currstatus = self._get_status(disk, realcfg)
        if currstatus.lower() == 'globalhotspare':
            return
        self._make_available(disk, realcfg)
        self._set_drive_state(disk, "Global")
    
    def _set_drive_state(self, disk, state):
        raid_alldevices = self.webclient.grab_json_response(
            '/api/providers/raid_alldevices')
        if raid_alldevices.get('return', -1) != 0:
            raise Exception(
                'Unexpected return to get all RAID devices information')
        for c in raid_alldevices.get('StorageComplexes',[]):
            cslot = str(c.get('SlotNumber'))
            if cslot == disk.id[0].split(',')[1]:
                c_pciaddr = c.get('PCIeAddress',-1)
                cdrives = c.get('Drives',[])
                for d in cdrives:
                    if disk.id[1] == d.get('Id',''):
                        currstatus = d['Oem']['Lenovo']['DriveStatus']
                        d_resid = d['Internal']['ResourceId']
                        if state in ("Global", "None"):
                            data = {
                                "controller_address": c_pciaddr,
                                "drive_resource_id": d_resid,
                                "hotspare_type": state,
                                "pool_resource_ids": []}
                            raidlink_url = '/api/providers/raidlink_AssignHotSpare'
                        else:
                            data = {
                                "controller_address": c_pciaddr,
                                "drive_operation": state,
                                "drive_resource_ids": [d_resid]}
                            raidlink_url = '/api/providers/raidlink_DiskStateAction'
                        msg = self._do_web_request(raidlink_url, method='POST', 
                                                   payload=data, cache=False)
                        if msg.get('return', -1) != 0:
                            raise Exception(
                                'Unexpected return to set disk state: {0}'.format(
                                msg.get('return', -1)))
                        set_state_token = msg.get('token', '')
                        msg = self._do_web_request(
                            '/api/providers/raidlink_QueryAsyncStatus',
                            method='POST',
                            payload={"token": set_state_token},
                            cache=False)
                        while msg['status'] == 2:
                            time.sleep(1)
                            msg = self._do_web_request(
                            '/api/providers/raidlink_QueryAsyncStatus',
                            method='POST',
                            payload={"token": set_state_token},
                            cache=False)
                        if msg.get('return',-1) != 0 or msg.get('status',-1) != 0:
                            raise Exception(
                                'Unexpected return to set disk state: {0}'.format(
                                msg.get('return', -1)))
                        disk_url=f"/redfish/v1/Systems/1/Storage/{disk.id[0].split(',')[0]}/Drives/{disk.id[1]}"
                        newstatus = self.webclient.grab_json_response(disk_url)
                        disk_converted = False
                        for _ in range(60):
                            if currstatus == newstatus['Oem']['Lenovo']['DriveStatus']:
                                time.sleep(1)
                                newstatus = self.webclient.grab_json_response(disk_url)
                            else:
                                disk_converted = True
                                break
                        if not disk_converted:
                            raise Exception(
                                'Disk set command was successful, but the disk state is unchanged')

    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 remove_storage_configuration(self, cfgspec):
        realcfg = self.get_storage_configuration(False)
        for pool in cfgspec.arrays:
            for volume in pool.volumes:
                cid = volume.id[0].split(',')[0]
                vid = volume.id[1]
                msg, code = self.webclient.grab_json_response_with_status(
                    f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/{vid}',
                    method='DELETE')
                if code == 500:
                    raise Exception(
                        'Unexpected return to volume deletion: ' + repr(msg))
        for disk in cfgspec.disks:
            self._make_available(disk, realcfg)

    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(
                    'No RAID Type supported on this controller')
            requestedlevel = str(arrayspec.raid)
            if requestedlevel not in raidmap:
                raise pygexc.InvalidParameterValue(
                    'Requested RAID Type "{0}" not available on this '
                    'controller. Allowed values are: {1}'.format(
                        requestedlevel, [k for k in raidmap]))
            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
            minimal_conditions = {
                "RAID0": (1,128,1),
                "RAID1": (2,2,1),
                "RAID1Triple": (3,3,1),
                "RAID10": (4,128,2),
                "RAID10Triple": (6,128,3),
                "RAID5": (3,128,1),
                "RAID50": (6,128,2),
                "RAID6": (4,128,1),
                "RAID60": (8, 128, 2)
            }
            raid_level = rdinfo[0]
            min_pd = minimal_conditions[raid_level][0]
            max_pd = minimal_conditions[raid_level][1]
            disk_multiplier = minimal_conditions[raid_level][2]
            if len(drives) < min_pd or \
            len(drives) > max_pd or \
            len(drives) % disk_multiplier != 0:
                raise pygexc.InvalidParameterValue(
                    f'Number of disks for {rdinfo} must be between {min_pd} and {max_pd} and be a multiple of {disk_multiplier}')
            if hotspares:
                hstr = '|'.join([str(x.id[1]) for x in hotspares]) + '|'
            else:
                hstr = ''
            drvstr = '|'.join([str(x.id[1]) for x in drives]) + '|'
            return {
                'controller': controller,
                'drives': drvstr,
                'hotspares': hstr,
                'raidlevel': rdlvl,
                'spans': spancount,
                'perspan': drivesperspan,
            }
        else:
            # TODO(Jarrod Johnson): adding new volume to
            #  existing array would be here
            pass
    
    def _raid_number_map(self, controller):
        themap = {}
        cid = controller.split(',')
        rsp = self.webclient.grab_json_response(
            '/redfish/v1/Systems/1/Storage/{0}'.format(cid[0]))
        for rt in rsp['StorageControllers'][0]['SupportedRAIDTypes']:
            rt_lower = rt.lower()
            mapdata = (rt, 1)
            themap[rt]=mapdata
            themap[rt_lower] = mapdata
            themap[rt_lower.replace('raid','r')] = mapdata
            themap[rt_lower.replace('raid','')] = mapdata
        return themap

    def _create_array(self, pool):
        params = self._parse_array_spec(pool)
        cid = params['controller'].split(',')[0]
        c_capabilities, code = self.webclient.grab_json_response_with_status(
            f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/Capabilities')
        if code == 404:
            c_capabilities, code = self.webclient.grab_json_response_with_status(
            f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/Oem/Lenovo/Capabilities')
            if code == 404:
                # If none of the endpoints exist, maybe it should be printed that
                # no capabilities found, therefore default values will be used
                # whatever they are
                pass
        volumes = pool.volumes
        drives = [d for d in params['drives'].split("|") if d != '']
        hotspares = [h for h in params['hotspares'].split("|") if h != '']
        raidlevel = params['raidlevel']
        nameappend = 1
        currvolnames = None
        currcfg = self.get_storage_configuration(False)
        for vol in volumes:
            if vol.name is None:
                # need to iterate while there exists a volume of that name
                if currvolnames is None:
                    currvolnames = set([])
                    for pool in currcfg.arrays:
                        for volume in pool.volumes:
                            currvolnames.add(volume.name)
                name = 'Volume_{0}'.format(nameappend)
                nameappend += 1
                while name in currvolnames:
                    name = 'Volume_{0}'.format(nameappend)
                    nameappend += 1
            else:
                name = vol.name

            # Won't check against Redfish allowable values as they not trustworthy yet
            # Some values show in Redfish, but may not be accepted by UEFI/controller or vice versa 
            stripsize_map = {
                '4': 4096, '4096': 4096,
                '16': 16384, '16384': 16384,
                '32': 32768, '32768': 32768,
                '64': 65536, '65536': 65536,
                '128': 131072, '131072': 131072,
                '256': 262144, '262144': 262144,
                '512': 524288, '524288': 524288,
                '1024': 1048576, '1048576': 1048576
            }
            stripsize = stripsize_map[str(vol.stripsize).lower().replace('k','')] if vol.stripsize is not None else None

            readpolicy_map = {'0': 'Off', '1': 'ReadAhead'}
            read_policy = None
            read_cache_possible = c_capabilities.get("ReadCachePolicy@Redfish.AllowableValues",[])
            if read_cache_possible:
                if vol.read_policy is not None:
                    if str(vol.read_policy) in readpolicy_map:
                        vol.read_policy = readpolicy_map[str(vol.read_policy)]
                    if vol.read_policy in read_cache_possible:
                        read_policy = vol.read_policy
                    else:
                        raise pygexc.InvalidParameterValue(
                        f'{vol.read_policy} Read Cache Policy is not supported. Allowed values are: {read_cache_possible}')
            
            writepolicy_map = {'0': 'WriteThrough', '1': 'UnprotectedWriteBack',
                               '2': 'ProtectedWriteBack', '3': 'Off'}
            write_policy = None
            write_cache_possible = c_capabilities.get("WriteCachePolicy@Redfish.AllowableValues",[])
            if write_cache_possible:
                if vol.write_policy is not None:
                    if str(vol.write_policy) in writepolicy_map:
                        vol.write_policy = writepolicy_map[str(vol.write_policy)]
                    if vol.write_policy in write_cache_possible:
                        write_policy = vol.write_policy
                    else:
                        raise pygexc.InvalidParameterValue(
                        f'{vol.write_policy} Write Cache Policy is not supported. Allowed values are: {write_cache_possible}')

            defaultinit_map = {'0': 'No', '1': 'Fast', '2': 'Full'}
            default_init = None
            default_init_possible = c_capabilities.get("InitializationType@Redfish.AllowableValues",[])
            if default_init_possible:
                if vol.default_init is not None:
                    if str(vol.default_init) in defaultinit_map:
                        vol.default_init = defaultinit_map[str(vol.default_init)]
                    if vol.default_init in default_init_possible:
                        default_init = vol.default_init
                    else:
                        raise pygexc.InvalidParameterValue(
                        f'{vol.default_init} Initialization Type is not supported. Allowed values are: {default_init_possible}')

            volsize = None
            spec_disks = sorted(drives)
            spec_hotspares = hotspares
            for array in currcfg.arrays:
                in_use_disks = sorted([d.id[1] for d in array.disks])
                in_use_hotspares = [h.id[1] for h in array.hotspares]
                if spec_disks == in_use_disks:
                    if vol.size is None:
                        volsize = array.available_capacity
                        array.available_capacity = 0
                        break
                    else:
                        strsize = str(vol.size)
                        if strsize in ('all','100%'):
                            raise pygexc.InvalidParameterValue(
                                f'Requested size for volume {name} exceeds available capacity. Available capacity is {array.available_capacity} MiB')
                        elif strsize.endswith('%'):
                            volsize = int(array.capacity
                                        * float(strsize.replace('%', ''))
                                        / 100.0)
                            if volsize > array.available_capacity:
                                raise pygexc.InvalidParameterValue(
                                f'Requested size for volume {name} exceeds available capacity. Available capacity is {array.available_capacity} MiB')
                            else:
                                array.available_capacity-=volsize
                        else:
                            try:
                                volsize = int(strsize)
                                if volsize > array.available_capacity:
                                    raise pygexc.InvalidParameterValue(
                                        f'Requested size for volume {name} exceeds available capacity. Available capacity is {array.available_capacity} MiB')
                                else:
                                    array.available_capacity-=volsize
                            except ValueError:
                                raise pygexc.InvalidParameterValue(
                                    'Unrecognized size ' + strsize)
                elif any(d in in_use_disks for d in spec_disks):
                    raise pygexc.InvalidParameterValue(
                        f'At least one disk from provided config is in use by another volume. To create a volume using the remaining capacity, configure the new volume to use all the following disks: {in_use_disks}')
                else:
                    disks_capacities = {}
                    for d in spec_disks:
                        disks_capacities[d] = self.webclient.grab_json_response(
                            f'/redfish/v1/Systems/1/Storage/{cid}/Drives/{d}')["CapacityBytes"]
                    max_capacity = sum(v for k,v in disks_capacities.items())
                    min_disk = min([v for k,v in disks_capacities.items()])
                    disk_count = len(disks_capacities)
                    max_capacity_per_raid = {
                        "RAID0": max_capacity,
                        "RAID1": min_disk,
                        "RAID1Triple": min_disk,
                        "RAID10": (disk_count//2)*min_disk,
                        "RAID10Triple": (disk_count//3)*min_disk,
                        "RAID5": (disk_count-1)*min_disk,
                        "RAID50": (disk_count-2)*min_disk,
                        "RAID6": (disk_count-2)*min_disk,
                        "RAID60": (disk_count-4)*min_disk
                        }
                    if vol.size is not None:
                        strsize = str(vol.size)
                        if strsize.endswith('%'):
                            volsize = int(max_capacity_per_raid[raidlevel]
                                        * float(strsize.replace('%', ''))
                                        / 100.0)
                        else:
                            try:
                                volsize = int(strsize)
                                if volsize > max_capacity_per_raid[raidlevel]:
                                    raise pygexc.InvalidParameterValue(
                                        f'Requested size for volume {name} exceeds available capacity. Available capacity is {max_capacity_per_raid[raidlevel]} bytes')
                            except ValueError:
                                raise pygexc.InvalidParameterValue(
                                    'Unrecognized size ' + strsize)
                for h in spec_hotspares:
                    if h in in_use_hotspares:
                        raise pygexc.InvalidParameterValue(
                            f'Hotspare {h} from provided config is in use by another volume.')
            
            request_data = {
                "Name":name,
                "RAIDType":raidlevel,
                "Links":{
                    "Drives":[
                        {'@odata.id': f'/redfish/v1/Systems/1/Storage/{cid}/Drives/{did}'} for did in spec_disks]}}
            if spec_hotspares:
                request_data["Links"]["DedicatedSpareDrives"] = {[
                    {'@odata.id': f'/redfish/v1/Systems/1/Storage/{cid}/Drives/{hid}' for hid in spec_hotspares}]}
            if volsize:
                request_data["CapacityBytes"] = volsize
            if stripsize:
                request_data["StripSizeBytes"] = stripsize
            if read_policy:
                request_data["ReadCachePolicy"] = read_policy
            if write_policy:
                request_data["WriteCachePolicy"] = write_policy

            msg, code=self.webclient.grab_json_response_with_status(
                f'/redfish/v1/Systems/1/Storage/{cid}/Volumes',
                method='POST',
                data=request_data)
            if code == 500:
                raise Exception("Unexpected response to volume creation: " + repr(msg))
            time.sleep(60)
            #Even if in web the volume appears immediately, get_storage_configuration does not see it that fast
            newcfg = self.get_storage_configuration(False)
            newvols = [v.id for p in newcfg.arrays for v in p.volumes]
            currvols = [v.id for p in currcfg.arrays for v in p.volumes]
            newvol = list(set(newvols) - set(currvols))[0]
            if default_init:
                msg, code = self.webclient.grab_json_response_with_status(
                    f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/{newvol[1]}/Actions/Volume.Initialize',
                    method='POST',
                    data = {"InitializeType": default_init})
                if code == 500:
                    raise Exception("Unexpected response to volume initialization: " + repr(msg))

    def attach_remote_media(self, url, user, password, vmurls):
        for vmurl in vmurls:
            if 'EXT' not in vmurl:
                continue
            vminfo = self._do_web_request(vmurl, cache=False)
            if vminfo['ConnectedVia'] != 'NotConnected':
                continue
            msg,code = self.webclient.grab_json_response_with_status(
                vmurl,
                data={'Image': url, 'Inserted': True},
                method='PATCH')
            if code == 500:
                raise Exception("Unexpected response when attaching remote media: " + repr(msg))
            raise pygexc.BypassGenericBehavior()
            break
        else:
            raise pygexc.InvalidParameterValue(
                'XCC does not have required license for operation')

    def get_screenshot(self, outfile):
        wc = self.webclient.dupe()
        self._get_session_token(wc)
        url = '/web_download/Mini_ScreenShot.jpg'
        fd = webclient.FileDownloader(wc, url, outfile)
        fd.start()
        fd.join()

    def get_diagnostic_data(self, savefile, progress=None, autosuffix=False):
        tsk = self._do_web_request(
            '/redfish/v1/Systems/1/LogServices/DiagnosticLog/Actions/LogService.CollectDiagnosticData',
            {"DiagnosticDataType": "Manager", "SelectDataTypes": []})
        taskrunning = True
        taskurl = tsk.get('TaskMonitor', None)
        pct = 0 if taskurl else 100
        durl = None
        while pct < 100 and taskrunning:
            status = self._do_web_request(taskurl)
            durl = status.get('AdditionalDataURI', '')
            pct = status.get('PercentComplete', 0)
            taskrunning = status.get('TaskState', 'Complete') == 'Running'
            if progress:
                progress({'phase': 'initializing', 'progress': float(pct)})
            if taskrunning:
                time.sleep(3)
        if not durl:
            raise Exception("Failed getting service data url")
        fname = os.path.basename(durl)
        if autosuffix and not savefile.endswith('.tar.zst'):
            savefile += '-{0}'.format(fname)
        fd = webclient.FileDownloader(self.webclient, durl, savefile)
        fd.start()
        while fd.isAlive():
            fd.join(1)
            if progress and self.webclient.get_download_progress():
                progress({'phase': 'download',
                          'progress': 100 * self.webclient.get_download_progress()})
        if fd.exc:
            raise fd.exc
        if progress:
            progress({'phase': 'complete'})
        return savefile

    def get_ikvm_methods(self):
        return ['openbmc', 'url']

    def get_ikvm_launchdata(self):
        access = self._do_web_request('/redfish/v1/Managers/1/Oem/Lenovo/RemoteControl/Actions/LenovoRemoteControlService.GetRemoteConsoleToken', {})
        if access.get('Token', None):
            accessinfo = {
                'url': '/#/login?{}&context=remote&mode=multi'.format(access['Token'])
                }
            return accessinfo

    def get_system_power_watts(self, fishclient):
        powerinfo = fishclient._do_web_request('/redfish/v1/Chassis/1/Sensors/power_Sys_Power')
        return powerinfo['Reading']

    def get_health(self, fishclient, verbose=True):
        rsp = self._do_web_request('/api/providers/imm_active_events')
        summary = {'badreadings': [], 'health': pygconst.Health.Ok}
        fallbackdata = []
        hmap = {
            0 : pygconst.Health.Ok,
            3: pygconst.Health.Critical,
            2: pygconst.Health.Warning,
        }
        infoevents = False
        existingevts = set([])
        for item in rsp.get('items', ()):
            # while usually the ipmi interrogation shall explain things,
            # just in case there is a gap, make sure at least the
            # health field is accurately updated
            itemseverity = hmap.get(item.get('Severity', 2),
                                    pygconst.Health.Critical)
            if itemseverity == pygconst.Health.Ok:
                infoevents = True
                continue
            if (summary['health'] < itemseverity):
                summary['health'] = itemseverity
            evtsrc = item.get('Oem', {}).get('Lenovo', {}).get('Source', '')
            currevt = '{}:{}'.format(evtsrc, item['Message'])
            if currevt in existingevts:
                continue
            existingevts.add(currevt)
            fallbackdata.append(SensorReading({
                'name': evtsrc,
                'states': [item['Message']],
                'health': itemseverity,
                'type': evtsrc,
            }, ''))
        summary['badreadings'] = fallbackdata
        return summary

    def _get_cpu_temps(self, fishclient):
        cputemps = []
        for reading in super()._get_cpu_temps(fishclient):
            if 'Margin' in reading['Name']:
                continue
            cputemps.append(reading)
        return cputemps

    def get_system_configuration(self, hideadvanced=True, fishclient=None):
        stgs = self._getsyscfg(fishclient)[0]
        outstgs = {}
        for stg in stgs:
            outstgs[f'UEFI.{stg}'] = stgs[stg]
        return outstgs

    def set_system_configuration(self, changeset, fishclient):
        bmchangeset = {}
        vpdchangeset = {}
        for stg in list(changeset):
            if stg.startswith('BMC.'):
                bmchangeset[stg.replace('BMC.', '')] = changeset[stg]
                del changeset[stg]
            if stg.startswith('UEFI.'):
                changeset[stg.replace('UEFI.', '')] = changeset[stg]
                del changeset[stg]
            if stg.startswith('VPD.'):
                vpdchangeset[stg.replace('VPD.', '')] = changeset[stg]
                del changeset[stg]
        if changeset:
            super().set_system_configuration(changeset, fishclient)
        if bmchangeset:
            self._set_xcc3_settings(bmchangeset, fishclient)
        if vpdchangeset:
            self._set_xcc3_vpd(vpdchangeset, fishclient)

    def _set_xcc3_vpd(self, changeset, fishclient):
        newvpd = {'Attributes': changeset}
        fishclient._do_web_request(
            '/redfish/v1/Chassis/1/Oem/Lenovo/SysvpdSettings/Actions/LenovoSysVpdSettings.SetVpdSettings',
            newvpd)


    def _set_xcc3_settings(self, changeset, fishclient):
        currsettings, reginfo = self._get_lnv_bmcstgs(fishclient)
        rawsettings = fishclient._do_web_request('/redfish/v1/Managers/1/Oem/Lenovo/BMCSettings',
                                                 cache=False)
        rawsettings = rawsettings.get('Attributes', {})
        pendingsettings = {}
        ret = self._set_redfish_settings(
            changeset, fishclient, currsettings, rawsettings,
            pendingsettings, self.lenovobmcattrdeps, reginfo,
            '/redfish/v1/Managers/1/Oem/Lenovo/BMCSettings')
        fishclient._do_web_request('/redfish/v1/Managers/1/Oem/Lenovo/BMCSettings', cache=False)
        return ret

    oemacctmap = {
        'password_reuse_count': 'MinimumPasswordReuseCycle',
        'password_change_interval':  'MinimumPasswordChangeIntervalHours',
        'password_expiration': 'PasswordExpirationPeriodDays',
        'password_complexity': 'ComplexPassword',
        }

    acctmap = {
        'password_login_failures': 'AccountLockoutThreshold',
        'password_min_length': 'MinPasswordLength',
        'password_lockout_period': 'AccountLockoutDuration',
        }

    def update_firmware(self, filename, data=None, progress=None, bank=None, otherfields=()):
        if not otherfields and bank == 'backup':
            uxzcount = 0
            otherfields = {'UpdateParameters': {"Targets": ["/redfish/v1/UpdateService/FirmwareInventory/BMC-Backup"]}}
            needseek = False
            if data and hasattr(data, 'read'):
                if zipfile.is_zipfile(data):
                    needseek = True
                    z = zipfile.ZipFile(data)
                else:
                    data.seek(0)
            elif data is None and zipfile.is_zipfile(filename):
                z = zipfile.ZipFile(filename)
            if z:
                for tmpname in z.namelist():
                    if tmpname.startswith('payloads/'):
                        uxzcount += 1
                        if tmpname.endswith('.uxz'):
                            wrappedfilename = tmpname
            if uxzcount == 1 and wrappedfilename:
                filename = os.path.basename(wrappedfilename)
                data = z.open(wrappedfilename)
            elif needseek:
                data.seek(0)
        super().update_firmware(filename, data=data, progress=progress, bank=bank, otherfields=otherfields)

    def get_bmc_configuration(self):
        settings = {}
        acctsrv = self._do_web_request('/redfish/v1/AccountService')
        for oemstg in self.oemacctmap:
            settings[oemstg] = {
                'value': acctsrv['Oem']['Lenovo'][self.oemacctmap[oemstg]]}
        for stg in self.acctmap:
            settings[stg] = {
                'value': acctsrv[self.acctmap[stg]]}
        bmcstgs = self._do_web_request('/redfish/v1/Managers/1/Oem/Lenovo/BMCSettings')
        bmcattrs = bmcstgs['Attributes']
        self.ethoverusb = True if 'EthOverUSBEnabled' in bmcattrs else False
        usbcfg = bmcattrs.get('NetMgrUsb0Enabled', bmcattrs.get('EthOverUSBEnabled', 'False'))
        usbeth = 'Enable' if usbcfg == 'True' else 'Disable'
        settings['usb_ethernet'] = {
            'value': usbeth
        }
        usbcfg = bmcattrs.get('NetMgrUsb0PortForwardingEnabled', bmcattrs.get('EthOverUSBPortForwardingEnabled', 'False'))
        fwd = 'Enable' if usbcfg == 'True' else 'Disable'
        settings['usb_ethernet_port_forwarding'] = fwd
        mappings = []
        for idx in range(1, 11):
            keyname = 'NetMgrUsb0PortForwardingPortMapping.{}'.format(idx)
            keyaltname = 'EthOverUSBPortForwardingPortMapping_{}'.format(idx)
            currval = bmcattrs.get(keyname, bmcattrs.get(keyaltname, '0,0'))
            if currval == '0,0':
                continue
            src, dst = currval.split(',')
            mappings.append('{}:{}'.format(src,dst))
        settings['usb_forwarded_ports'] = {'value': ','.join(mappings)}
        cfgin = self._get_lnv_bmcstgs(self)[0]
        for stgname in cfgin:
            settings[f'{stgname}'] = cfgin[stgname]
        return settings

    def set_bmc_configuration(self, changeset):
        acctattribs = {}
        usbsettings = {}
        bmchangeset = {}
        rawchangeset = {}
        for key in changeset:
            if isinstance(changeset[key], str):
                rawchangeset[key] = changeset[key]
                changeset[key] = {'value': changeset[key]}
            currval = changeset[key].get('value', None)
            if key == 'password_complexity':
                if currval.lower() in ("false", 0):
                    currval = False
                elif currval.lower() in ('true', 1):
                    currval = True
            elif key.lower().startswith('usb_'):
                if 'forwarded_ports' not in key.lower():
                    currval = currval.lower()
                    if currval and 'disabled'.startswith(currval):
                        currval = 'False'
                    elif currval and 'enabled'.startswith(currval):
                        currval = 'True'
            else:
                try:
                    currval = int(currval)
                except ValueError:
                    pass
            if key.lower() in self.oemacctmap:
                if 'Oem' not in acctattribs:
                    acctattribs['Oem'] = {'Lenovo': {}}
                acctattribs['Oem']['Lenovo'][
                    self.oemacctmap[key.lower()]] = currval
                if key.lower() == 'password_expiration':
                    warntime = int(int(currval) * 0.08)
                    acctattribs['Oem']['Lenovo'][
                        'PasswordExpirationWarningPeriod'] = warntime
            elif key.lower() in self.acctmap:
                acctattribs[self.acctmap[key.lower()]] = currval
            elif key.lower() in (
                    'usb_ethernet', 'usb_ethernet_port_forwarding',
                    'usb_forwarded_ports'):
                usbsettings[key] = currval
            else:
                bmchangeset[key.replace('bmc.', '')] = rawchangeset[key]
        if acctattribs:
            self._do_web_request(
                '/redfish/v1/AccountService', acctattribs, method='PATCH')
            self._do_web_request('/redfish/v1/AccountService', cache=False)
        if usbsettings:
            self.apply_usb_configuration(usbsettings)
        if bmchangeset:
            self._set_xcc3_settings(bmchangeset, self)

    def apply_usb_configuration(self, usbsettings):
        bmcattribs = {}
        if not hasattr(self, 'ethoverusb'):
            self.get_bmc_configuration()

        if 'usb_forwarded_ports' in usbsettings:
            pairs = usbsettings['usb_forwarded_ports'].split(',')
            idx = 1
            for pair in pairs:
                if self.ethoverusb:
                    keyname = 'EthOverUSBPortForwardingPortMapping_{}'.format(idx)
                else:
                    keyname = 'NetMgrUsb0PortForwardingPortMapping.{}'.format(idx)
                pair = pair.replace(':', ',')
                bmcattribs[keyname] = pair
                idx += 1
            while idx < 11:
                if self.ethoverusb:
                    keyname = 'EthOverUSBPortForwardingPortMapping_{}'.format(idx)
                else:
                    keyname = 'NetMgrUsb0PortForwardingPortMapping.{}'.format(idx)
                bmcattribs[keyname] = '0,0'
                idx += 1
        if 'usb_ethernet' in usbsettings:
            keyname = 'EthOverUSBEnabled' if self.ethoverusb else 'NetMgrUsb0Enabled'
            bmcattribs[keyname] = usbsettings['usb_ethernet']
        if 'usb_ethernet_port_forwarding' in usbsettings:
            keyname = 'EthOverUSBPortForwardingEnabled' if self.ethoverusb else 'NetMgrUsb0PortForwardingEnabled'
            bmcattribs[keyname] = usbsettings[
                    'usb_ethernet_port_forwarding']
        self._do_web_request(
            '/redfish/v1/Managers/1/Oem/Lenovo/BMCSettings',
            {'Attributes': bmcattribs}, method='PATCH')
        self._do_web_request(
            '/redfish/v1/Managers/1/Oem/Lenovo/BMCSettings', cache=False)

    def get_extended_bmc_configuration(self, fishclient, hideadvanced=True):
        cfgin = self._get_lnv_bmcstgs(fishclient)[0]
        cfgout = {}
        for stgname in cfgin:
            cfgout[f'BMC.{stgname}'] = cfgin[stgname]
        vpdin = self._get_lnv_vpd(fishclient)[0]
        for stgname in vpdin:
            cfgout[f'VPD.{stgname}'] = vpdin[stgname]
        return cfgout

    def _get_lnv_vpd(self, fishclient):
        currsettings, reginfo = self._get_lnv_stgs(
            fishclient, '/redfish/v1/Chassis/1/Oem/Lenovo/SysvpdSettings')
        self.lenovobmcattrdeps = reginfo[3]
        return currsettings, reginfo

    def _get_lnv_bmcstgs(self, fishclient):
        currsettings, reginfo = self._get_lnv_stgs(
            fishclient, '/redfish/v1/Managers/1/Oem/Lenovo/BMCSettings')
        self.lenovobmcattrdeps = reginfo[3]
        return currsettings, reginfo

    def _get_lnv_stgs(self, fishclient, url):
        bmcstgs = fishclient._do_web_request(url)
        bmcreg = bmcstgs.get('AttributeRegistry', None)
        extrainfo = {}
        valtodisplay = {}
        currsettings = {}
        reginfo = {}, {}, {}, {}
        if bmcreg:
            reginfo = self._get_attrib_registry(fishclient, bmcreg)
            if reginfo:
                extrainfo, valtodisplay, _, _ = reginfo
        for setting in bmcstgs.get('Attributes', {}):
            val = bmcstgs['Attributes'][setting]
            currval = val
            val = valtodisplay.get(setting, {}).get(val, val)
            val = {'value': val}
            val.update(**extrainfo.get(setting, {}))
            currsettings[setting] = val
        return currsettings, reginfo

    def get_description(self, fishclient):
        rsp = self._get_expanded_data('/redfish/v1/Chassis')
        for chassis in rsp['Members']:
            if (chassis['@odata.id'] == '/redfish/v1/Chassis/1'
                    and chassis['ChassisType'] != 'Blade'):
                hmm = chassis.get('HeightMm', None)
                if hmm:
                    return {'height': hmm/44.45}
            if (chassis['@odata.id'] == '/redfish/v1/Chassis/Enclosure'
                    and chassis.get('ChassisType', None) == 'Enclosure'):
                try:
                    slot = chassis['Location']['PartLocation']['LocationOrdinalValue']
                    slotnum = (2 * (slot >> 4) - 1) + ((slot & 15) % 10)
                    slotcoord = [slot >> 4, (slot & 15) - 9]
                    return {'slot': slotnum, 'slotlabel': '{:02x}'.format(slot), 'slotcoord': slotcoord}
                except KeyError:
                    continue
        return {}

    def upload_media(self, filename, progress=None, data=None):
        wc = self.webclient
        uploadthread = webclient.FileUploader(
            wc, '/rdoc_upload', filename, data,
            formname='file',
            formwrap=True)
        uploadthread.start()
        while uploadthread.isAlive():
            uploadthread.join(3)
            if progress:
                progress({'phase': 'upload',
                          'progress': 100 * wc.get_upload_progress()})
        rsp = json.loads(uploadthread.rsp)
        if rsp['return'] != 0:
            raise Exception('Issue uploading file')
        remfilename = rsp['upload_filename']
        if progress:
            progress({'phase': 'upload',
                      'progress': 100.0})
        self._do_web_request(
            '/redfish/v1/Systems/1/VirtualMedia/RDOC1',
            {'Image':'file:///gpx/rdocupload/' + remfilename,
             'WriteProtected': False}, method='PATCH')
        if progress:
            progress({'phase': 'complete'})

    def get_firmware_inventory(self, components, fishclient, category):
        sfs = fishclient._do_web_request('/api/providers/system_firmware_status')
        pendingscm = sfs.get('fpga_scm_pending_build', None)
        pendinghpm = sfs.get('fpga_hpm_pending_build', None)
        if pendingscm == '*':
            pendingscm = None
        if pendinghpm == '*':
            pendinghpm = None
        fwlist = fishclient._do_web_request(fishclient._fwinventory + '?$expand=.')
        fwlist = copy.deepcopy(fwlist.get('Members', []))
        self._fwnamemap = {}
        for redres in fwlist:
            fwurl = redres['@odata.id']
            res = (redres, fwurl)
            if fwurl.startswith('/redfish/v1/UpdateService/FirmwareInventory/Bundle.'):
                continue  # skip Bundle information for now
            if redres.get('Name', '').startswith('Firmware:'):
                redres['Name'] = redres['Name'].replace('Firmware:', '')
            if redres['Name'].startswith('Firmware-PSoC') and 'Drive_Backplane' in redres["@odata.id"]:
                redres['Name'] = 'Drive Backplane'
            if redres['Name'].startswith('DEVICE-'):
                redres['Name'] = redres['Name'].replace('DEVICE-', '')
            if redres['Name'].startswith('POWER-PSU'):
                redres['Name'] = redres['Name'].replace('POWER-', '')
            swid = redres.get('SoftwareId', '')
            buildid = ''
            version = redres.get('Version', None)
            for prefix in ['FPGA-', 'UEFI-', 'BMC-', 'LXPM-', 'DRVWN-', 'DRVLN-', 'LXUM']:
                if swid.startswith(prefix):
                    buildid = swid.split('-')[1] + version.split('-')[0]
                    version = '-'.join(version.split('-')[1:])
                    break
            if version:
                redres['Version'] = version
            cres = fishclient._extract_fwinfo(res)
            if cres[0] is None:
                continue
            if buildid:
                cres[1]['build'] = buildid
            yield cres
            if cres[0] == 'SCM-FPGA' and pendingscm:
                yield 'SCM-FPGA Pending', {
                    'Name': 'SCM-FPGA Pending',
                    'build': pendingscm}
            elif cres[0] == 'HPM-FPGA' and pendinghpm:
                yield 'HPM-FPGA Pending', {
                    'Name': 'HPM-FPGA Pending',
                    'build': pendinghpm}
        raise pygexc.BypassGenericBehavior()
