Skip to content

Commit 9cc44c7

Browse files
committed
feat(redfish): port vendor value quirks into projection functions
Detect the vendor per member from its Oem block and apply quirks ported 1:1 from bb-Ricardo/check_redfish: - get_systems_memory: Dell iDRAC 8 module-size division, HPE DIMMStatus, Fujitsu SignalStatus/LegacyStatus and generic top-level DIMMStatus folded into Status_State/Status_Health. - get_chassis_thermal_fans: normalize fan speed reported in RPM or percent. - get_systems_storage_drives: expose drive temperature (Celsius). - get_chassis_power_powercontrol: new projection for the overall power consumption entry. bumps __version__ to 2026060704.
1 parent 8b6b7bb commit 9cc44c7

3 files changed

Lines changed: 320 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
### Added
1212

1313
* redfish.py: `get_auth_header()` builds the request authentication header, reusing a cached session token to avoid creating a new controller session on every request, and falling back to HTTP Basic auth
14+
* redfish.py: `get_chassis_power_powercontrol()` parses a power control (overall power consumption) entry for health monitoring and reporting
1415
* redfish.py: `get_manager()` parses a manager (BMC) resource for health monitoring and identification
1516
* redfish.py: `get_systems_ethernetinterfaces()` parses an Ethernet interface resource for health monitoring and identification
16-
* redfish.py: `get_systems_memory()` parses a memory module (DIMM) resource for health monitoring and identification
17+
* redfish.py: `get_systems_memory()` parses a memory module (DIMM) resource for health monitoring and identification, applying vendor quirks (Dell module size, HPE/Fujitsu OEM module status)
1718
* redfish.py: `get_systems_processors()` parses a processor (CPU) resource for health monitoring and identification
1819
* redfish.py: `get_systems_storage_volumes()` parses a volume (logical drive) resource for health monitoring and identification
1920
* redfish.py: `get_updateservice_firmwareinventory()` parses a firmware inventory resource for version reporting and health monitoring
2021

2122
### Changed
2223

24+
* redfish.py: `get_chassis_thermal_fans()` normalizes fan speed reported in RPM or percent onto a single shape
2325
* redfish.py: `get_manager_logservices_sel_entries()` can filter log entries by regular expression and age out entries older than a cutoff
24-
* redfish.py: `get_systems_storage_drives()` also extracts `PowerOnHours`, so consumers can report drive age
26+
* redfish.py: `get_systems_storage_drives()` also extracts `PowerOnHours` and the drive temperature, so consumers can report drive age and temperature
2527
* time.py: `timestr2epoch()` accepts `pattern='iso8601'` to parse ISO 8601 timestamps (trailing `Z`, embedded offset or date-only) without specifying the exact layout
2628
* url.py: `fetch_json()` accepts a `retries` argument to re-attempt a failed request or an unparseable body, for flaky endpoints
2729

redfish.py

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"""This library parses data returned from the Redfish API."""
1212

1313
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
14-
__version__ = '2026060703'
14+
__version__ = '2026060704'
1515

1616
import base64
1717

@@ -59,6 +59,18 @@
5959
'Status_HealthRollup': ('Status', 'HealthRollup'),
6060
}
6161

62+
CHASSIS_POWER_CONTROL_KEYS = (
63+
'MemberId',
64+
'Name',
65+
'PowerCapacityWatts',
66+
'PowerConsumedWatts',
67+
)
68+
69+
CHASSIS_POWER_CONTROL_NESTED_KEYS = {
70+
'Status_State': ('Status', 'State'),
71+
'Status_Health': ('Status', 'Health'),
72+
}
73+
6274
CHASSIS_POWER_KEYS = (
6375
'FirmwareVersion',
6476
'LastPowerOutputWatts',
@@ -216,6 +228,16 @@
216228
'Status_HealthRollup': ('Status', 'HealthRollup'),
217229
}
218230

231+
# Some controllers leave the standard Status.State/Health empty on memory
232+
# modules and report the real condition only in an OEM-specific field. These
233+
# tables fold those vendor operational values back onto the standard Redfish
234+
# vocabulary so the generic get_state() can evaluate them. Modules in an absent
235+
# state are skipped by the callers; healthy operational states map to "Enabled",
236+
# everything else is surfaced as a problem.
237+
MEMORY_OEM_ABSENT_STATES = ('Absent', 'EmptyOrNotInstalled', 'NotPresent')
238+
MEMORY_OEM_HEALTHY_HEALTH = ('enabled', 'nominal', 'ok')
239+
MEMORY_OEM_HEALTHY_STATES = ('Enabled', 'GoodInUse', 'Operable', 'Quiesced')
240+
219241
PROCESSOR_KEYS = (
220242
'Id',
221243
'InstructionSet',
@@ -466,6 +488,46 @@ def get_chassis(redfish):
466488
return data
467489

468490

491+
def get_chassis_power_powercontrol(redfish):
492+
"""
493+
Extract power control (overall power consumption) information from a Redfish API response.
494+
495+
The legacy Power resource exposes one or more `PowerControl` entries that report the aggregate
496+
power consumption of the chassis. This function projects a single such entry into a flat
497+
dictionary.
498+
499+
### Parameters
500+
- **redfish** (`dict`): A dictionary containing a single Redfish `PowerControl` entry.
501+
502+
### Returns
503+
- **dict**: A dictionary containing the following power control details:
504+
- **MemberId** (`str`): The identifier of the power control entry.
505+
- **Name** (`str`): The name of the power control entry.
506+
- **PowerCapacityWatts** (`str`): The total power capacity in watts.
507+
- **PowerConsumedWatts** (`str`): The currently consumed power in watts.
508+
- **Status_State** (`str`): The state of the power control entry (e.g., "Enabled").
509+
- **Status_Health** (`str`): The health status of the power control entry (e.g., "OK").
510+
511+
### Example
512+
>>> redfish_data = {
513+
... 'Name': 'System Power Control',
514+
... 'PowerConsumedWatts': 344,
515+
... 'Status': {'State': 'Enabled', 'Health': 'OK'},
516+
... }
517+
>>> get_chassis_power_powercontrol(redfish_data)
518+
{'Name': 'System Power Control', 'PowerConsumedWatts': 344, ..., 'Status_State': 'Enabled', ...}
519+
"""
520+
data = {key: redfish.get(key, '') for key in CHASSIS_POWER_CONTROL_KEYS}
521+
522+
for out_key, path in CHASSIS_POWER_CONTROL_NESTED_KEYS.items():
523+
ref = redfish
524+
for step in path:
525+
ref = ref.get(step, {})
526+
data[out_key] = ref if isinstance(ref, (str, int, float)) else ''
527+
528+
return data
529+
530+
469531
def get_chassis_power_powersupplies(redfish):
470532
"""
471533
Extract power supply information from a Redfish API response.
@@ -643,6 +705,16 @@ def get_chassis_thermal_fans(redfish):
643705
ref = ref.get(step, {})
644706
data[out_key] = ref if isinstance(ref, (str, int, float)) else ''
645707

708+
# vendor quirk: Dell, Fujitsu and Huawei report fan speed in RPM under
709+
# "ReadingRPM"; HP and Lenovo report a percentage. Normalize both onto
710+
# Reading / ReadingUnits so get_perfdata() and the table see one shape.
711+
if redfish.get('ReadingRPM') is not None or redfish.get('ReadingUnits') == 'RPM':
712+
reading = redfish.get('ReadingRPM')
713+
data['Reading'] = redfish.get('Reading', '') if reading is None else reading
714+
data['ReadingUnits'] = 'RPM'
715+
elif redfish.get('ReadingUnits') == 'Percent':
716+
data['ReadingUnits'] = '%'
717+
646718
return data
647719

648720

@@ -1275,8 +1347,55 @@ def get_systems_memory(redfish):
12751347
ref = ref.get(step, {})
12761348
data[out_key] = ref if isinstance(ref, (str, int, float)) else ''
12771349

1278-
capacity = redfish.get('CapacityMiB')
1279-
data['CapacityMiB'] = human.bytes2human(capacity * 1024 * 1024) if capacity else ''
1350+
# the vendor is detected from the member's own Oem block, so the projection
1351+
# stays self-contained (dict in, dict out)
1352+
vendor = get_vendor(redfish)
1353+
1354+
# vendor quirk: some controllers report the module size as "SizeMB"; Dell
1355+
# iDRAC 8 even reports decimal MB instead of binary MiB, so a value that is
1356+
# not a clean MiB multiple is converted back to a MiB count.
1357+
capacity = redfish.get('SizeMB') or redfish.get('CapacityMiB')
1358+
if capacity:
1359+
capacity = int(capacity)
1360+
if vendor == 'dell' and capacity % 1024 != 0:
1361+
capacity = round(capacity * 1024**2 / 1000**2)
1362+
data['CapacityMiB'] = human.bytes2human(capacity * 1024 * 1024)
1363+
else:
1364+
data['CapacityMiB'] = ''
1365+
1366+
# vendor quirk: when the standard Status block is empty, fold the
1367+
# OEM-specific status field into Status_State / Status_Health.
1368+
oem = redfish.get('Oem') or {}
1369+
oem_block = next(iter(oem.values()), {}) if isinstance(oem, dict) else {}
1370+
if not isinstance(oem_block, dict):
1371+
oem_block = {}
1372+
1373+
oem_state = ''
1374+
if vendor == 'hpe':
1375+
oem_state = oem_block.get('DIMMStatus', '')
1376+
elif vendor == 'fujitsu' and oem_block.get('SignalStatus'):
1377+
oem_state = oem_block.get('SignalStatus', '')
1378+
# Fujitsu reports the health verdict separately in LegacyStatus
1379+
legacy = oem_block.get('LegacyStatus')
1380+
if legacy:
1381+
data['Status_Health'] = (
1382+
'OK' if legacy.lower() in MEMORY_OEM_HEALTHY_HEALTH else 'Critical'
1383+
)
1384+
elif redfish.get('DIMMStatus'):
1385+
oem_state = redfish.get('DIMMStatus', '')
1386+
1387+
if oem_state:
1388+
if oem_state in MEMORY_OEM_ABSENT_STATES:
1389+
data['Status_State'] = 'Absent'
1390+
elif oem_state in MEMORY_OEM_HEALTHY_STATES:
1391+
data['Status_State'] = 'Enabled'
1392+
if not data['Status_Health']:
1393+
data['Status_Health'] = 'OK'
1394+
else:
1395+
# an unrecognized operational value means the module needs attention
1396+
data['Status_State'] = 'Enabled'
1397+
if data['Status_Health'] in ('', 'OK'):
1398+
data['Status_Health'] = 'Critical'
12801399

12811400
return data
12821401

@@ -1455,6 +1574,21 @@ def get_systems_storage_drives(redfish):
14551574
capacity = redfish.get('CapacityBytes')
14561575
data['CapacityBytes'] = human.bytes2human(capacity) if capacity else ''
14571576

1577+
# vendor quirk: drive temperature is not a standard Drive property. HPE
1578+
# SmartStorage exposes "CurrentTemperatureCelsius"; other vendors put it in
1579+
# their OEM block as TemperatureCelsius / TemperatureC. Expose it (in
1580+
# degrees Celsius) so it can be trended as a gauge.
1581+
oem = redfish.get('Oem') or {}
1582+
oem_block = next(iter(oem.values()), {}) if isinstance(oem, dict) else {}
1583+
if not isinstance(oem_block, dict):
1584+
oem_block = {}
1585+
temperature = (
1586+
redfish.get('CurrentTemperatureCelsius')
1587+
or oem_block.get('TemperatureCelsius')
1588+
or oem_block.get('TemperatureC')
1589+
)
1590+
data['Temperature'] = temperature if isinstance(temperature, (int, float)) else ''
1591+
14581592
return data
14591593

14601594

0 commit comments

Comments
 (0)