Skip to content

Commit

Permalink
Make map_jbof async and process JBOFs in parallel
Browse files Browse the repository at this point in the history
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").
  • Loading branch information
bmeagherix committed Jun 26, 2024
1 parent c3a25b2 commit 41be87f
Show file tree
Hide file tree
Showing 5 changed files with 451 additions and 187 deletions.
13 changes: 6 additions & 7 deletions src/middlewared/middlewared/plugins/enclosure_/enclosure2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
Empty file.
140 changes: 140 additions & 0 deletions src/middlewared/middlewared/plugins/enclosure_/jbof/es24n.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 41be87f

Please sign in to comment.