|
11 | 11 | """This library parses data returned from the Redfish API.""" |
12 | 12 |
|
13 | 13 | __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland' |
14 | | -__version__ = '2026060703' |
| 14 | +__version__ = '2026060704' |
15 | 15 |
|
16 | 16 | import base64 |
17 | 17 |
|
|
59 | 59 | 'Status_HealthRollup': ('Status', 'HealthRollup'), |
60 | 60 | } |
61 | 61 |
|
| 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 | + |
62 | 74 | CHASSIS_POWER_KEYS = ( |
63 | 75 | 'FirmwareVersion', |
64 | 76 | 'LastPowerOutputWatts', |
|
216 | 228 | 'Status_HealthRollup': ('Status', 'HealthRollup'), |
217 | 229 | } |
218 | 230 |
|
| 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 | + |
219 | 241 | PROCESSOR_KEYS = ( |
220 | 242 | 'Id', |
221 | 243 | 'InstructionSet', |
@@ -466,6 +488,46 @@ def get_chassis(redfish): |
466 | 488 | return data |
467 | 489 |
|
468 | 490 |
|
| 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 | + |
469 | 531 | def get_chassis_power_powersupplies(redfish): |
470 | 532 | """ |
471 | 533 | Extract power supply information from a Redfish API response. |
@@ -643,6 +705,16 @@ def get_chassis_thermal_fans(redfish): |
643 | 705 | ref = ref.get(step, {}) |
644 | 706 | data[out_key] = ref if isinstance(ref, (str, int, float)) else '' |
645 | 707 |
|
| 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 | + |
646 | 718 | return data |
647 | 719 |
|
648 | 720 |
|
@@ -1275,8 +1347,55 @@ def get_systems_memory(redfish): |
1275 | 1347 | ref = ref.get(step, {}) |
1276 | 1348 | data[out_key] = ref if isinstance(ref, (str, int, float)) else '' |
1277 | 1349 |
|
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' |
1280 | 1399 |
|
1281 | 1400 | return data |
1282 | 1401 |
|
@@ -1455,6 +1574,21 @@ def get_systems_storage_drives(redfish): |
1455 | 1574 | capacity = redfish.get('CapacityBytes') |
1456 | 1575 | data['CapacityBytes'] = human.bytes2human(capacity) if capacity else '' |
1457 | 1576 |
|
| 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 | + |
1458 | 1592 | return data |
1459 | 1593 |
|
1460 | 1594 |
|
|
0 commit comments