From ec1407881a3b7d4bb57701d1af75d56d2a5eea6f Mon Sep 17 00:00:00 2001 From: Brian Meagher Date: Wed, 26 Jun 2024 08:59:27 -0700 Subject: [PATCH] Make map_jbof async and process JBOFs in parallel Refactor code and in an async map_jbof now call map_es24n in parallel. This will use the new AsyncRedfishClient to obtain various information from the JBOF redfish interface, which will then be mapped to a similar dict as is done for other enclosure types. This commit adds "Power Supply", "Cooling", "Temperature Sensors" and "Voltage Sensor" to the elements returned by map_es24n (was already returning "Array Device Slot"). --- .../plugins/enclosure_/enclosure2.py | 13 +- .../plugins/enclosure_/jbof/es24n.py | 140 +++++++++ .../plugins/enclosure_/jbof/utils.py | 269 ++++++++++++++++++ .../plugins/enclosure_/jbof_enclosures.py | 216 +++----------- 4 files changed, 451 insertions(+), 187 deletions(-) create mode 100644 src/middlewared/middlewared/plugins/enclosure_/jbof/es24n.py create mode 100644 src/middlewared/middlewared/plugins/enclosure_/jbof/utils.py diff --git a/src/middlewared/middlewared/plugins/enclosure_/enclosure2.py b/src/middlewared/middlewared/plugins/enclosure_/enclosure2.py index 279fb6cbe7b07..e7c125b7322e5 100644 --- a/src/middlewared/middlewared/plugins/enclosure_/enclosure2.py +++ b/src/middlewared/middlewared/plugins/enclosure_/enclosure2.py @@ -5,17 +5,17 @@ from libsg3.ses import EnclosureDevice -from middlewared.schema import accepts, Dict, Str, Int +from middlewared.schema import Dict, Int, Str, accepts from middlewared.service import Service, filterable from middlewared.service_exception import MatchNotFound, ValidationError from middlewared.utils import filter_list from .constants import SUPPORTS_IDENTIFY_KEY +from .fseries_drive_identify import set_slot_status as fseries_set_slot_status from .jbof_enclosures import map_jbof from .map2 import combine_enclosures from .nvme2 import map_nvme from .r30_drive_identify import set_slot_status as r30_set_slot_status -from .fseries_drive_identify import set_slot_status as fseries_set_slot_status from .ses_enclosures2 import get_ses_enclosures @@ -49,14 +49,14 @@ def get_ses_enclosures(self, dmi=None): dmi = self.middleware.call_sync('system.dmidecode_info')['system-product-name'] return get_ses_enclosures(dmi) - def map_jbof(self, jbof_qry=None): + async def map_jbof(self, jbof_qry=None): """This method serves as an endpoint to easily be able to test the JBOF mapping logic specifically without having to call enclosure2.query which includes the head-unit and all other attached JBO{D/F}s. """ if jbof_qry is None: - jbof_qry = self.middleware.call_sync('jbof.query') - return map_jbof(jbof_qry) + jbof_qry = await self.middleware.call('jbof.query') + return await map_jbof(jbof_qry) def map_nvme(self, dmi=None): """This method serves as an endpoint to easily be able to test @@ -147,8 +147,7 @@ def query(self, filters, options): for label in self.middleware.call_sync('datastore.query', 'truenas.enclosurelabel') } dmi = self.middleware.call_sync('system.dmidecode_info')['system-product-name'] - jbofs = self.middleware.call_sync('jbof.query') - for i in self.get_ses_enclosures(dmi) + self.map_nvme(dmi) + self.map_jbof(jbofs): + for i in self.get_ses_enclosures(dmi) + self.map_nvme(dmi) + self.middleware.call_sync('enclosure2.map_jbof'): if i.pop('should_ignore'): continue diff --git a/src/middlewared/middlewared/plugins/enclosure_/jbof/es24n.py b/src/middlewared/middlewared/plugins/enclosure_/jbof/es24n.py new file mode 100644 index 0000000000000..7be375257a094 --- /dev/null +++ b/src/middlewared/middlewared/plugins/enclosure_/jbof/es24n.py @@ -0,0 +1,140 @@ +from logging import getLogger + +from middlewared.plugins.enclosure_.enums import ElementType, JbofModels +from middlewared.plugins.enclosure_.jbof.utils import (fake_jbof_enclosure, + map_cooling, + map_power_supplies, + map_temperature_sensors, + map_voltage_sensors) +from middlewared.plugins.jbof.functions import get_sys_class_nvme + +LOGGER = getLogger(__name__) + + +async def map_es24n(model, rclient, uri): + data = {} + urls = {'Drives': f'{uri}/Drives?$expand=*', + 'PowerSubsystem': f'{uri}/PowerSubsystem?$expand=*($levels=2)', + 'Sensors': f'{uri}/Sensors?$expand=*', + 'ThermalSubsystem': f'{uri}/ThermalSubsystem?$expand=*($levels=2)' + } + try: + # Unfortunately the ES24n response doesn't lend itself it issuing a single query. + # + # Furthermore, experiments have shown that executing the queries in series + # is just as fast as executing in parallel, so we'll do the former here for + # simplicity. + for key, uri2 in urls.items(): + info = await rclient.get(uri2) + if not info: + LOGGER.error('Unexpected failure fetching %r info', key, exc_info=True) + return + data[key] = info + except Exception: + LOGGER.error('Unexpected failure enumerating all enclosure info', exc_info=True) + return + return do_map_es24n(model, rclient.uuid, data) + + +def do_map_es24n(model, uuid, data): + # + # Drives + # + try: + all_disks = data['Drives'] + except KeyError: + LOGGER.error('Unexpected failure extracting all disk info', exc_info=True) + return + + num_of_slots = len(all_disks['Members']) + ui_info = { + 'rackmount': True, + 'top_loaded': False, + 'front_slots': num_of_slots, + 'rear_slots': 0, + 'internal_slots': 0 + } + mounted_disks = { + v['serial']: (k, v) for k, v in get_sys_class_nvme().items() + if v['serial'] and v['transport_protocol'] == 'rdma' + } + mapped = dict() + for disk in all_disks['Members']: + slot = disk.get('Id', '') + if not slot or not slot.isdigit(): + # shouldn't happen but need to catch edge-case + continue + else: + slot = int(slot) + + state = disk.get('Status', {}).get('State') + if not state or state == 'Absent': + mapped[slot] = None + continue + + sn = disk.get('SerialNumber') + if not sn: + mapped[slot] = None + continue + + if found := mounted_disks.get(sn): + try: + # we expect namespace 1 for the device (i.e. nvme1n1) + idx = found[1]['namespaces'].index(f'{found[0]}n1') + mapped[slot] = found[1]['namespaces'][idx] + except ValueError: + mapped[slot] = None + else: + mapped[slot] = None + + elements = {} + if psus := map_power_supplies(data): + elements[ElementType.POWER_SUPPLY.value] = psus + if cooling := map_cooling(data): + elements[ElementType.COOLING.value] = cooling + if temperature := map_temperature_sensors(data): + elements[ElementType.TEMPERATURE_SENSORS.value] = temperature + if voltage := map_voltage_sensors(data): + elements[ElementType.VOLTAGE_SENSOR.value] = voltage + # No Current Sensors reported + + return fake_jbof_enclosure(model, uuid, num_of_slots, mapped, ui_info, elements) + + +async def is_this_an_es24n(rclient): + """At time of writing, we've discovered that OEM of the ES24N + does not give us predictable model names. Seems to be random + which is unfortunate but there isn't much we can do about it + at the moment. We know what the URI _should_ be for this + platform and we _thought_ we knew what the model should be so + we'll hard-code these values and check for the specific URI + and then check if the model at the URI at least has some + semblance of an ES24N""" + # FIXME: This function shouldn't exist and the OEM should fix + # this at some point. When they do (hopefully) fix the model, + # remove this function + expected_uri = '/redfish/v1/Chassis/2U24' + expected_model = JbofModels.ES24N.value + try: + info = await rclient.get(expected_uri) + if info: + found_model = info.get('Model', '').lower() + eml = expected_model.lower() + if any(( + eml in found_model, + found_model.startswith(eml), + found_model.startswith(eml[:-1]) + )): + # 1. the model string is inside the found model + # 2. or the model string startswith what we expect + # 3. or the model string startswith what we expect + # with the exception of the last character + # (The reason why we chop off last character is + # because internal conversation concluded that the + # last digit coorrelates to "generation" so we're + # going to be extra lenient and ignore it) + return JbofModels.ES24N.name, expected_uri + except Exception: + LOGGER.error('Unexpected failure determining if this is an ES24N', exc_info=True) + + return None, None diff --git a/src/middlewared/middlewared/plugins/enclosure_/jbof/utils.py b/src/middlewared/middlewared/plugins/enclosure_/jbof/utils.py new file mode 100644 index 0000000000000..51f4f45ceaf6d --- /dev/null +++ b/src/middlewared/middlewared/plugins/enclosure_/jbof/utils.py @@ -0,0 +1,269 @@ +from logging import getLogger + +from middlewared.plugins.enclosure_.enums import (ElementStatus, + RedfishStatusHealth, + RedfishStatusState) +from middlewared.plugins.enclosure_.slot_mappings import get_jbof_slot_info + +LOGGER = getLogger(__name__) + + +def fake_jbof_enclosure(model, uuid, num_of_slots, mapped, ui_info, elements={}): + """This function takes the nvme devices that been mapped + to their respective slots and then creates a "fake" enclosure + device that matches (similarly) to what our real enclosure + mapping code does (get_ses_enclosures()). It's _VERY_ important + that the keys in the `fake_enclosure` dictionary exist because + our generic enclosure mapping logic expects certain top-level + keys. + + Furthermore, we generate DMI (SMBIOS) information for this + "fake" enclosure because our enclosure mapping logic has to have + a guaranteed unique key for each enclosure so it can properly + map the disks accordingly + """ + # TODO: The `fake_enclosure` object should be removed from this + # function and should be generated by the + # `plugins.enclosure_/enclosure_class.py:Enclosure` class so we + # can get rid of duplicate logic in this module and in that class + fake_enclosure = { + 'id': uuid, + 'dmi': uuid, + 'model': model, + 'should_ignore': False, + 'sg': None, + 'bsg': None, + 'name': f'{model} JBoF Enclosure', + 'controller': False, + 'status': ['OK'], + 'elements': {'Array Device Slot': {}} + } + disks_map = get_jbof_slot_info(model) + if not disks_map: + fake_enclosure['should_ignore'] = True + return [fake_enclosure] + + fake_enclosure.update(ui_info) + + for slot in range(1, num_of_slots + 1): + device = mapped.get(slot, None) + # the `value_raw` variables represent the + # value they would have if a device was + # inserted into a proper SES device (or not). + # Since this is NVMe (which deals with PCIe) + # that paradigm doesn't exist per se but we're + # "faking" a SES device, hence the hex values. + # The `status` variables use same logic. + if device is not None: + status = 'OK' + value_raw = 0x1000000 + else: + status = 'Not installed' + value_raw = 0x5000000 + + mapped_slot = disks_map['versions']['DEFAULT']['model'][model][slot]['mapped_slot'] + fake_enclosure['elements']['Array Device Slot'][mapped_slot] = { + 'descriptor': f'Disk #{slot}', + 'status': status, + 'value': None, + 'value_raw': value_raw, + 'dev': device, + 'original': { + 'enclosure_id': uuid, + 'enclosure_sg': None, + 'enclosure_bsg': None, + 'descriptor': f'slot{slot}', + 'slot': slot, + } + } + + for element_type in elements: + if elements[element_type]: + fake_enclosure['elements'][element_type] = elements[element_type] + + return [fake_enclosure] + + +def map_redfish_status_to_status(status): + """Return a status string based upon the Redfish Status""" + + if state := status.get('State'): + if state == RedfishStatusState.ABSENT.value: + return ElementStatus.NOT_INSTALLED.value + + if health := status.get('Health'): + match health: + case RedfishStatusHealth.CRITICAL.value: + return ElementStatus.CRITICAL.value + case RedfishStatusHealth.OK.value: + return ElementStatus.OK.value + case RedfishStatusHealth.WARNING.value: + return ElementStatus.NONCRITICAL.value + case _: + return ElementStatus.UNKNOWN.value + return ElementStatus.UNKNOWN.value + + +def map_redfish_to_value(data, keys): + """Return a value which is a comma seperated string of all the values + present in data.""" + # It was decided NOT to try to map these to SES-like values, as this + # would introduce an impedance mismatch when we circle back to the + # Redfish provider again. + values = [] + for key in keys: + if val := data.get(key): + values.append(val) + return ', '.join(values) or None + + +def map_redfish_psu_to_value(psu): + """Return a value string corresponding to the redfish data""" + # Just use LineInputStatus (DSP0268_2024.1 6.103.5.2 LineInputStatus) + return map_redfish_to_value(psu, ['LineInputStatus']) + + +def map_redfish_psu(psu): + """Utility function to map a Redfish PSU data to our enclosure services format""" + # Redfish Data Model Specification https://www.dmtf.org/dsp/DSP0268 + # DSP0268_2024.1 6.103 PowerSupply 1.6.0 + # DSP0268_2023.2 6.103 PowerSupply 1.5.2 + # DSP0268_2023.1 6.97 PowerSupply 1.5.1 + # ... + # For ES24n implemented with @odata.type = #PowerSupply.v1_5_1.PowerSupply + # + # Example data from redfish + # {'@odata.id': '/redfish/v1/Chassis/2U24/PowerSubsystem/PowerSupplies/PSU1', + # '@odata.type': '#PowerSupply.v1_5_1.PowerSupply', + # 'Actions': {'#PowerSupply.Reset': {'ResetType@Redfish.AllowableValues': ['On','ForceOff'], + # 'target': '/redfish/v1/Chassis/2U24/PowerSubsystem/PowerSupplies/PSU1/Actions/PowerSupply.Reset'}}, + # 'FirmwareVersion': 'A00', + # 'Id': 'PSU1', + # 'LineInputStatus': 'Normal', + # 'Manufacturer': '3Y POWER', + # 'Model': 'YSEF1600EM-2A01P10', + # 'Name': 'PSU1', + # 'PowerCapacityWatts': 1600, + # 'SerialNumber': 'S0A00A3032029000265', + # 'Status': {'Health': 'OK', + # 'State': 'Enabled'}}, + desc_fields = ['Name', 'Model', 'SerialNumber', 'FirmwareVersion', 'Manufacturer'] + desc = [psu.get(k, '') for k in desc_fields] + if watt := psu.get('PowerCapacityWatts'): + desc.append(f'{watt}W') + return { + 'descriptor': ','.join(desc), + "status": map_redfish_status_to_status(psu['Status']), + "value": map_redfish_psu_to_value(psu), + "value_raw": None + } + + +def map_power_supplies(data): + result = {} + for member in data['PowerSubsystem']['PowerSupplies']['Members']: + ident = member.get('Id') + if ident: + result[ident] = map_redfish_psu(member) + return result + + +def map_redfish_fan_to_value(data): + values = [] + if speedpercent := data.get('SpeedPercent'): + if speedrpm := speedpercent.get('SpeedRPM'): + values.append(f'SpeedRPM={speedrpm}') + if location_indicator_active := data.get('LocationIndicatorActive'): + if location_indicator_active: + values.append('LocationIndicatorActive') + return ', '.join(values) or None + + +def map_redfish_fan(data): + # Example data from redfish + # {'@odata.id': '/redfish/v1/Chassis/2U24/ThermalSubsystem/Fans/Fan1', + # '@odata.type': '#Fan.v1_4_0.Fan', + # 'Id': 'Fan1', + # 'LocationIndicatorActive': False, + # 'Name': 'Fan1', + # 'SpeedPercent': {'DataSourceUri': '/redfish/v1/Chassis/2U24/Sensors/Fan1', 'SpeedRPM': 9920.0}, + # 'Status': {'Health': 'OK', 'State': 'Enabled'}} + return { + 'descriptor': data.get('Name'), + "status": map_redfish_status_to_status(data['Status']), + "value": map_redfish_fan_to_value(data), + "value_raw": None + } + + +def map_cooling(data): + result = {} + for member in data['ThermalSubsystem']['Fans']['Members']: + ident = member.get('Id') + if ident: + result[ident] = map_redfish_fan(member) + return result + + +def map_redfish_sensor_to_value(data): + if reading := data.get('Reading'): + if units := data.get('ReadingUnits'): + return f'{reading} {units}' + else: + # Make sure it's a string + return f'{reading}' + + +def map_redfish_temperature_sensor(data): + # Example data from redfish + # {'@odata.id': '/redfish/v1/Chassis/2U24/Sensors/TempDrive1', + # '@odata.type': '#Sensor.v1_6_0.Sensor', + # 'Id': 'TempDrive1', + # 'Name': 'Temperature Sensor Drive 1', + # 'Reading': 26.0, + # 'ReadingType': 'Temperature', + # 'ReadingUnits': 'C', + # 'Status': {'Health': 'OK', 'State': 'Enabled'}}, + return { + 'descriptor': data.get('Name'), + "status": map_redfish_status_to_status(data['Status']), + "value": map_redfish_sensor_to_value(data), + "value_raw": None + } + + +def map_temperature_sensors(data): + result = {} + for member in data['Sensors']['Members']: + ident = member.get('Id') + reading_type = member.get('ReadingType') + if ident and reading_type == 'Temperature': + result[ident] = map_redfish_temperature_sensor(member) + return result + + +def map_redfish_voltage_sensor(data): + # Example data from redfish + # {'@odata.id': '/redfish/v1/Chassis/2U24/Sensors/VoltPS1Vin', + # '@odata.type': '#Sensor.v1_6_0.Sensor', + # 'Id': 'VoltPS1Vin', + # 'Name': 'VoltPS1Vin', + # 'Reading': 206.0, + # 'ReadingType': 'Voltage', + # 'Status': {'Health': 'OK', 'State': 'Enabled'}}, + return { + 'descriptor': data.get('Name'), + "status": map_redfish_status_to_status(data['Status']), + "value": map_redfish_sensor_to_value(data), + "value_raw": None + } + + +def map_voltage_sensors(data): + result = {} + for member in data['Sensors']['Members']: + ident = member.get('Id') + reading_type = member.get('ReadingType') + if ident and reading_type == 'Voltage': + result[ident] = map_redfish_voltage_sensor(member) + return result diff --git a/src/middlewared/middlewared/plugins/enclosure_/jbof_enclosures.py b/src/middlewared/middlewared/plugins/enclosure_/jbof_enclosures.py index 68a8f49ec5fcd..d44c78964c807 100644 --- a/src/middlewared/middlewared/plugins/enclosure_/jbof_enclosures.py +++ b/src/middlewared/middlewared/plugins/enclosure_/jbof_enclosures.py @@ -1,206 +1,48 @@ +import asyncio from logging import getLogger from middlewared.plugins.enclosure_.enums import JbofModels -from middlewared.plugins.enclosure_.slot_mappings import get_jbof_slot_info -from middlewared.plugins.jbof.functions import get_sys_class_nvme -from middlewared.plugins.jbof.redfish import RedfishClient, InvalidCredentialsError +from middlewared.plugins.enclosure_.jbof.es24n import (is_this_an_es24n, + map_es24n) +from middlewared.plugins.jbof.redfish import (AsyncRedfishClient, + InvalidCredentialsError) LOGGER = getLogger(__name__) -def fake_jbof_enclosure(model, uuid, num_of_slots, mapped, ui_info): - """This function takes the nvme devices that been mapped - to their respective slots and then creates a "fake" enclosure - device that matches (similarly) to what our real enclosure - mapping code does (get_ses_enclosures()). It's _VERY_ important - that the keys in the `fake_enclosure` dictionary exist because - our generic enclosure mapping logic expects certain top-level - keys. - - Furthermore, we generate DMI (SMBIOS) information for this - "fake" enclosure because our enclosure mapping logic has to have - a guaranteed unique key for each enclosure so it can properly - map the disks accordingly - """ - # TODO: The `fake_enclosure` object should be removed from this - # function and should be generated by the - # `plugins.enclosure_/enclosure_class.py:Enclosure` class so we - # can get rid of duplicate logic in this module and in that class - fake_enclosure = { - 'id': uuid, - 'dmi': uuid, - 'model': model, - 'should_ignore': False, - 'sg': None, - 'bsg': None, - 'name': f'{model} JBoF Enclosure', - 'controller': False, - 'status': ['OK'], - 'elements': {'Array Device Slot': {}} - } - disks_map = get_jbof_slot_info(model) - if not disks_map: - fake_enclosure['should_ignore'] = True - return [fake_enclosure] - - fake_enclosure.update(ui_info) - - for slot in range(1, num_of_slots + 1): - device = mapped.get(slot, None) - # the `value_raw` variables represent the - # value they would have if a device was - # inserted into a proper SES device (or not). - # Since this is NVMe (which deals with PCIe) - # that paradigm doesn't exist per se but we're - # "faking" a SES device, hence the hex values. - # The `status` variables use same logic. - if device is not None: - status = 'OK' - value_raw = 0x1000000 - else: - status = 'Not installed' - value_raw = 0x5000000 - - mapped_slot = disks_map['versions']['DEFAULT']['model'][model][slot]['mapped_slot'] - fake_enclosure['elements']['Array Device Slot'][mapped_slot] = { - 'descriptor': f'Disk #{slot}', - 'status': status, - 'value': None, - 'value_raw': value_raw, - 'dev': device, - 'original': { - 'enclosure_id': uuid, - 'enclosure_sg': None, - 'enclosure_bsg': None, - 'descriptor': f'slot{slot}', - 'slot': slot, - } - } - - return [fake_enclosure] - - -def map_es24n(model, rclient, uri): - try: - all_disks = rclient.get(f'{uri}/Drives?$expand=*').json() - except Exception: - LOGGER.error('Unexpected failure enumerating all disk info', exc_info=True) - return - - num_of_slots = all_disks['Members@odata.count'] - ui_info = { - 'rackmount': True, - 'top_loaded': False, - 'front_slots': num_of_slots, - 'rear_slots': 0, - 'internal_slots': 0 - } - mounted_disks = { - v['serial']: (k, v) for k, v in get_sys_class_nvme().items() - if v['serial'] and v['transport_protocol'] == 'rdma' - } - mapped = dict() - for disk in all_disks['Members']: - slot = disk.get('Id', '') - if not slot or not slot.isdigit(): - # shouldn't happen but need to catch edge-case - continue - else: - slot = int(slot) - - state = disk.get('Status', {}).get('State') - if not state or state == 'Absent': - mapped[slot] = None - continue - - sn = disk.get('SerialNumber') - if not sn: - mapped[slot] = None - continue - - if found := mounted_disks.get(sn): - try: - # we expect namespace 1 for the device (i.e. nvme1n1) - idx = found[1]['namespaces'].index(f'{found[0]}n1') - mapped[slot] = found[1]['namespaces'][idx] - except ValueError: - mapped[slot] = None - else: - mapped[slot] = None - - return fake_jbof_enclosure(model, rclient.uuid, num_of_slots, mapped, ui_info) - - -def get_redfish_clients(jbofs): +JBOF_MODEL_ATTR = 'model' +JBOF_URI_ATTR = 'uri' + + +async def get_redfish_clients(jbofs): clients = dict() for jbof in jbofs: try: - rclient = RedfishClient( - f'https://{jbof["mgmt_ip1"]}', jbof['mgmt_username'], jbof['mgmt_password'] - ) - clients[jbof['mgmt_ip1']] = rclient + rclient = await AsyncRedfishClient.cache_get(jbof['uuid'], jbofs) + clients[jbof['uuid']] = rclient except InvalidCredentialsError: - LOGGER.error('Failed to login to redfish ip %r', jbof['mgmt_ip1']) + LOGGER.error('Failed to login to redfish ip %r %r', jbof['mgmt_ip1'], jbof['mgmt_ip2']) except Exception: LOGGER.error('Unexpected failure creating redfish client object', exc_info=True) return clients -def is_this_an_es24n(rclient): - """At time of writing, we've discovered that OEM of the ES24N - does not give us predictable model names. Seems to be random - which is unfortunate but there isn't much we can do about it - at the moment. We know what the URI _should_ be for this - platform and we _thought_ we knew what the model should be so - we'll hard-code these values and check for the specific URI - and then check if the model at the URI at least has some - semblance of an ES24N""" - # FIXME: This function shouldn't exist and the OEM should fix - # this at some point. When they do (hopefully) fix the model, - # remove this function - expected_uri = '/redfish/v1/Chassis/2U24' - expected_model = JbofModels.ES24N.value - try: - info = rclient.get(expected_uri) - if info.ok: - found_model = info.json().get('Model', '').lower() - eml = expected_model.lower() - if any(( - eml in found_model, - found_model.startswith(eml), - found_model.startswith(eml[:-1]) - )): - # 1. the model string is inside the found model - # 2. or the model string startswith what we expect - # 3. or the model string startswith what we expect - # with the exception of the last character - # (The reason why we chop off last character is - # because internal conversation concluded that the - # last digit coorrelates to "generation" so we're - # going to be extra lenient and ignore it) - return JbofModels.ES24N.name, expected_uri - except Exception: - LOGGER.error('Unexpected failure determining if this is an ES24N', exc_info=True) - - return None, None - - -def get_enclosure_model(rclient): +async def get_enclosure_model(rclient): model = uri = None try: - chassis = rclient.chassis() + chassis = await rclient.chassis() except Exception: LOGGER.error('Unexpected failure enumerating chassis info', exc_info=True) return model, uri - model, uri = is_this_an_es24n(rclient) + model, uri = await is_this_an_es24n(rclient) if all((model, uri)): return model, uri try: for _, uri in chassis.items(): - info = rclient.get(uri) + info = await rclient.get(uri) if info.ok: try: model = JbofModels(info.json().get('Model', '')).name @@ -216,11 +58,25 @@ def get_enclosure_model(rclient): return model, uri -def map_jbof(jbof_query): +async def map_jbof(jbof_query): result = list() - for mgmt_ip, rclient in filter(lambda x: x[1] is not None, get_redfish_clients(jbof_query).items()): - model, uri = get_enclosure_model(rclient) - if model == JbofModels.ES24N.name and (mapped := map_es24n(model, rclient, uri)): - result.extend(mapped) + futures = [] + for rclient in (await get_redfish_clients(jbof_query)).values(): + # Since we're *already* keeping a client object around, cache a couple + # of attributes to make things faster after the first time. + model = rclient.get_attribute(JBOF_MODEL_ATTR) + uri = rclient.get_attribute(JBOF_URI_ATTR) + if not model or not uri: + model, uri = await get_enclosure_model(rclient) + rclient.set_attribute(JBOF_MODEL_ATTR, model) + rclient.set_attribute(JBOF_URI_ATTR, uri) + + if model == JbofModels.ES24N.name: + futures.append(map_es24n(model, rclient, uri)) + + # Now fetch the data from each JBOF in parallel + for ans in await asyncio.gather(*futures, return_exceptions=True): + if ans and not isinstance(ans, Exception): + result.extend(ans) return result