From 3a4338365b82f03c2d7c635d4082b0b96fe18bf1 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 16 Jan 2014 16:06:36 -0500 Subject: [PATCH] Add support for retrieving SDR data This patch implements SDR retrieval and the foundation to use the data to translate raw sensor reading data to usable information. Change-Id: Ic77fcde6a283a2ee7745a9c159038d2655911b0a --- pyghmi/constants.py | 20 ++ pyghmi/exceptions.py | 5 + pyghmi/ipmi/private/constants.py | 198 ++++++++++ pyghmi/ipmi/sdr.py | 600 +++++++++++++++++++++++++++++++ 4 files changed, 823 insertions(+) create mode 100644 pyghmi/constants.py create mode 100644 pyghmi/ipmi/sdr.py diff --git a/pyghmi/constants.py b/pyghmi/constants.py new file mode 100644 index 00000000..c9272b7c --- /dev/null +++ b/pyghmi/constants.py @@ -0,0 +1,20 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 IBM 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. + + +class Health: + Ok = 0 + Warning, Critical, Failed = [2**x for x in range(0, 3)] diff --git a/pyghmi/exceptions.py b/pyghmi/exceptions.py index eb7ff4fd..b0935fff 100644 --- a/pyghmi/exceptions.py +++ b/pyghmi/exceptions.py @@ -28,3 +28,8 @@ class IpmiException(PyghmiException): class InvalidParameterValue(PyghmiException): pass + + +class BmcErrorException(IpmiException): + # This denotes when library detects an invalid BMC behavior + pass diff --git a/pyghmi/ipmi/private/constants.py b/pyghmi/ipmi/private/constants.py index 0921ee03..bd1edda6 100644 --- a/pyghmi/ipmi/private/constants.py +++ b/pyghmi/ipmi/private/constants.py @@ -26,6 +26,204 @@ payload_types = { 'rakp4': 0x15, } +#sensor type codes, table 42-3 +sensor_type_codes = { + 1: 'Temperature', + 2: 'Voltage', + 3: 'Current', + 4: 'Fan', + 5: 'Chassis Intrusion', + 6: 'Platform Security', + 7: 'Processor', + 8: 'Power Supply', + 9: 'Power Unit', + 0xa: 'Cooling Device', + 0xb: 'Other', + 0xc: 'Memory', + 0xd: 'Drive Bay', + 0xe: 'POST Memory Resize', + 0xf: 'System Firmware Progress', + 0x10: 'Event Log Disabled', + 0x11: 'Watchdog', + 0x12: 'System Event', + 0x13: 'Critical interrupt', + 0x14: 'Button/switch', + 0x15: 'Module/Board', + 0x16: 'Microcontroller/Coprocessor', + 0x17: 'Add-in Card', + 0x18: 'Chassis', + 0x19: 'Chip Set', + 0x1a: 'Other FRU', + 0x1b: 'Cable/Interconnect', + 0x1c: 'Terminator', + 0x1d: 'System Boot', + 0x1e: 'Boot Error', + 0x1f: 'OS Boot', + 0x20: 'OS Stop', + 0x21: 'Slot/Connector', + 0x22: 'System ACPI Power State', + 0x23: 'Watchdog', + 0x24: 'Platform alert', + 0x25: 'Entity Presence', + 0x26: 'Monitor ASIC/IC', + 0x27: 'LAN', + 0x28: 'Management Subsystem Health', + 0x29: 'Battery', + 0x2a: 'Session Audit', + 0x2b: 'Version Change', + 0x2c: 'FRU State', +} + + +sensor_type_offsets = { + 1: 'Temperature', + 2: 'Voltage', + 3: 'Current', + 4: 'Fan', + 5: { + 0: 'General Chassis Intrusion', + 1: 'Drive Bay intrusion', + 2: 'I/O Card area intrusion', + 3: 'Processor area intrusion', + 4: 'Lost LAN connection', + 5: 'Unauthorized dock', + 6: 'Fan area intrusion', + }, + 6: { + 0: 'Front Panel Lockout Violation attempt', + 1: 'Pre-boot password violation - user', + 2: 'Pre-boot password violation - setup', + 3: 'Pre-boot password violation - netboot', + 4: 'Pre-boot password violation', + 5: 'Out-of-band access password violation', + }, + 7: { + 0: 'processor IERR', + 1: 'processor thermal trip', + 2: 'processor FRB1/BIST failure', + 3: 'processor FRB2/Hang in POST failure', + 4: 'processor FRB3/processor startup failure', + 5: 'processor configuration error', + 6: 'uncorrectable cpu complex error', + 7: 'processor presence detected', + 8: 'processor disabled', + 9: 'processor terminator presence detected', + 0xa: 'processor throttled', + 0xb: 'uncorrectable machine check exception', + 0xc: 'correctable machine check exception', + }, + 8: { + 0: 'power supply presence detected', + 1: 'power supply failure', + 2: 'power supply predictive failure', + 3: 'power supply input lost', + 4: 'power supply input out of range or lost', + 5: 'power supply input out of range', + 6: 'power supply configuration error', # event data 3 available + }, + 9: { + 0: 'power off/down', + 1: 'power cycle', + 2: '240VA power down', + 3: 'interlock power down', + 4: 'power input lost', + 5: 'soft power control failure', + 6: 'power unit failure', + 7: 'power unit predictive failure', + }, + 0xa: 'cooling device', + 0xb: 'units based sensor', + 0xc: { + 0: 'correctable memory error', + 1: 'uncorrectable memory error', + 2: 'memory parity', + 3: 'memory scrub failed', + 4: 'memory device disabled', + 5: 'correctable memory error logging limit reached', + 6: 'memory presence detected', + 7: 'memory configuration error', + 8: 'spare memory', # event data 3 available + 9: 'memory throttled', + 0xa: 'critical memory overtemperature', + }, + 0xd: { + 0: 'drive presence', + 1: 'drive fault', + 2: 'predictive drive failure', + 3: 'hot spare drive', + 4: 'drive consitency check in progress', + 5: 'drive in critical array', + 6: 'drive in failed array', + 7: 'rebuild in progress', + 8: 'rebuild aborted', + }, + 0xe: 'POST memory resize', +} + + +#entity ids from table 43-13 entity id codes +entity_ids = { + 0x0: 'unspecified', + 0x1: 'other', + 0x2: 'unknown', + 0x3: 'processor', + 0x4: 'disk or disk bay', + 0x5: 'peripheral bay', + 0x6: 'system management module', + 0x7: 'system board', + 0x8: 'memory module', + 0x9: 'processor module', + 0xa: 'power supply', + 0xb: 'add-in card', + 0xc: 'front panel board', + 0xd: 'back panel board', + 0xe: 'power system board', + 0xf: 'drive backplane', + 0x10: 'system internal expansion board', + 0x11: 'other system board', + 0x12: 'processor board', + 0x13: 'power unit / power domain', + 0x14: 'power module / DC-to-DC converter', + 0x15: 'power management /power distribution board', + 0x16: 'chassis back panel board', + 0x17: 'system chassis', + 0x18: 'sub-chassis', + 0x19: 'other chassis board', + 0x1a: 'disk drive bay', + 0x1b: 'peripheral bay', + 0x1c: 'device bay', + 0x1d: 'fan/cooling device', + 0x1e: 'cooling unit / cooling domain', + 0x1f: 'cable / interconnect', + 0x20: 'memory device', + 0x21: 'system management software', + 0x22: 'system firmware', + 0x23: 'operating system', + 0x24: 'system bus', + 0x25: 'group', + 0x26: 'remote management communication device', + 0x27: 'external environment', + 0x28: 'battery', + 0x29: 'processing blade', + 0x2a: 'connectivity switch', + 0x2b: 'processor/memory module', + 0x2c: 'I/O module', + 0x2d: 'Processor I/O module', + 0x2e: 'management controller firmware', + 0x2f: 'IPMI channel', + 0x30: 'PCI Bus', + 0x31: 'PCIe Bus', + 0x32: 'SCSI Bus', + 0x33: 'SATA/SAS Bus', + 0x34: 'processor / front-side bus', + 0x35: 'real time clock', + 0x37: 'air inlet', + 0x40: 'air inlet', + 0x41: 'processor', + 0x42: 'system board', +} + + rmcp_codes = { 1: ("Insufficient resources to create new session (wait for existing " "sessions to timeout)"), diff --git a/pyghmi/ipmi/sdr.py b/pyghmi/ipmi/sdr.py new file mode 100644 index 00000000..254cdb7d --- /dev/null +++ b/pyghmi/ipmi/sdr.py @@ -0,0 +1,600 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# coding=utf8 + +# Copyright 2013 IBM 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. + +# This module provides access to SDR offered by a BMC +# This data is common between 'sensors' and 'inventory' modules since SDR +# is both used to enumerate sensors for sensor commands and FRU ids for FRU +# commands + +# For now, we will not offer persistent SDR caching as we do in xCAT's IPMI +# code. Will see if it is adequate to advocate for high object reuse in a +# persistent process for the moment. + +# Focus is at least initially on the aspects that make the most sense for a +# remote client to care about. For example, smbus information is being +# skipped for now + +import math +import pyghmi.constants as const +import pyghmi.exceptions as exc +import pyghmi.ipmi.command as ipmicmd +import pyghmi.ipmi.private.constants as ipmiconstants +import struct + +TYPE_UNKNOWN = 0 +TYPE_SENSOR = 1 +TYPE_FRU = 2 + + +def ones_complement(value, bits): + # utility function to help with the large amount of 2s + # complement prevalent in ipmi spec + signbit = 0b1 << (bits - 1) + if value & signbit: + #if negative, subtract 1, then take 1s + #complement given bits width + return 0 - (value ^ ((0b1 << bits) - 1)) + else: + return value + + +def twos_complement(value, bits): + # utility function to help with the large amount of 2s + # complement prevalent in ipmi spec + signbit = 0b1 << (bits - 1) + if value & signbit: + #if negative, subtract 1, then take 1s + #complement given bits width + return 0 - ((value - 1) ^ ((0b1 << bits) - 1)) + else: + return value + + +unit_types = { + # table 43-15 'sensor unit type codes' + 0: '', + 1: '°C', + 2: '°F', + 3: 'K', + 4: 'V', + 5: 'A', + 6: 'W', + 7: 'J', + 8: 'C', + 9: 'VA', + 10: 'nt', + 11: 'lm', + 12: 'lx', + 13: 'cd', + 14: 'kPa', + 15: 'PSI', + 16: 'N', + 17: 'CFM', + 18: 'RPM', + 19: 'Hz', + 20: 'μs', + 21: 'ms', + 22: 's', + 23: 'min', + 24: 'hr', + 25: 'd', + 26: 'week(s)', + 27: 'mil', + 28: 'inches', + 29: 'ft', + 30: 'cu in', + 31: 'cu feet', + 32: 'mm', + 33: 'cm', + 34: 'm', + 35: 'cu cm', + 36: 'cu m', + 37: 'L', + 38: 'fl. oz.', + 39: 'radians', + 40: 'steradians', + 41: 'revolutions', + 42: 'cycles', + 43: 'g', + 44: 'ounce', + 45: 'lb', + 46: 'ft-lb', + 47: 'oz-in', + 48: 'gauss', + 49: 'gilberts', + 50: 'henry', + 51: 'millihenry', + 52: 'farad', + 53: 'microfarad', + 54: 'ohms', + 55: 'siemens', + 56: 'mole', + 57: 'becquerel', + 58: 'ppm', + 60: 'dB', + 61: 'dBA', + 62: 'dBC', + 63: 'Gy', + 64: 'sievert', + 65: 'color temp deg K', + 66: 'bit', + 67: 'kb', + 68: 'mb', + 69: 'gb', + 70: 'byte', + 71: 'kB', + 72: 'mB', + 73: 'gB', + 74: 'word', + 75: 'dword', + 76: 'qword', + 77: 'line', + 78: 'hit', + 79: 'miss', + 80: 'retry', + 81: 'reset', + 82: 'overrun/overflow', + 83: 'underrun', + 84: 'collision', + 85: 'packets', + 86: 'messages', + 87: 'characters', + 88: 'error', + 89: 'uncorrectable error', + 90: 'correctable error', + 91: 'fatal error', + 92: 'grams', +} + +sensor_rates = { + 0: '', + 1: ' per us', + 2: ' per ms', + 3: ' per s', + 4: ' per minute', + 5: ' per hour', + 6: ' per day', +} + + +class SensorReading(object): + """Representation of the state of a sensor. + + It is initialized by pyghmi internally, it does not make sense for + a developer to create one of these objects directly. + + It provides the following properties: + name: UTF-8 string describing the sensor + units: UTF-8 string describing the units of the sensor (if numeric) + value: Value of the sensor if numeric + imprecision: The amount by which the actual measured value may deviate from + 'value' due to limitations in the resolution of the given sensor. + """ + + def __init__(self, reading, suffix): + self.health = const.Health.Ok + self.type = reading['type'] + if 'health' in reading: + self.health = reading['health'] + if 'value' in reading: + self.value = reading['value'] + else: + self.value = None + self.states = reading['states'] + if 'unavailable' in reading: + self.unavailable = 1 + self.units = suffix + self.name = reading['name'] + self.imprecision = reading['imprecision'] + + def __repr__(self): + return repr({ + 'value': self.value, + 'states': self.states, + 'units': self.units, + 'imprecision': self.imprecision, + 'name': self.name, + 'type': self.type, + 'health': self.health + }) + + def _prettyprint(self): + repr = self.name + ": " + if self.value is not None: + repr += str(self.value) + repr += " ± " + str(self.imprecision) + repr += self.units + for state in self.states: + repr += state + "," + if self.health >= const.Health.Failed: + repr += '(Failed)' + elif self.health >= const.Health.Critical: + repr += '(Critical)' + elif self.health >= const.Health.Warning: + repr += '(Warning)' + return repr + + +class SDREntry(object): + """Represent a single entry in the IPMI SDR. + + This is created and consumed by pyghmi internally, there is no reason for + external code to pay attention to this class. + """ + + def __init__(self, entrybytes, reportunsupported=False): + # ignore record id for now, we only care about the sensor number for + # moment + self.reportunsupported = reportunsupported + if entrybytes[2] != 0x51: + # only recognize '1.5', the only version defined at time of writing + raise NotImplementedError + self.rectype = entrybytes[3] + self.linearization = None + #most important to get going are 1, 2, and 11 + self.sdrtype = TYPE_SENSOR # assume a sensor + if self.rectype == 1: # full sdr + self.full_decode(entrybytes[5:]) + elif self.rectype == 2: # full sdr + self.compact_decode(entrybytes[5:]) + elif self.rectype == 8: # entity association + self.association_decode(entrybytes[5:]) + elif self.rectype == 0x11: # FRU locator + self.fru_decode(entrybytes[5:]) + elif self.rectype == 0x12: # Management controller + self.mclocate_decode(entrybytes[5:]) + elif self.rectype == 0xc0: # OEM format + self.sdrtype = TYPE_UNKNOWN # assume undefined + self.oem_decode(entrybytes[5:]) + elif self.reportunsupported: + #will remove once I see it stop being thrown for now + #perhaps need some explicit mode to check for + #unsupported things, but make do otherwise + raise NotImplementedError + else: + self.sdrtype = TYPE_UNKNOWN + + @property + def name(self): + if self.sdrtype == TYPE_SENSOR: + return self.sensor_name + elif self.sdrtype == TYPE_FRU: + return self.fru_name + else: + return "UNKNOWN" + + def oem_decode(self, entry): + mfgid = entry[0] + (entry[1] << 8) + (entry[2] << 16) + if self.reportunsupported: + raise NotImplementedError("No support for mfgid %X" % mfgid) + + def mclocate_decode(self, entry): + # For now, we don't have use for MC locator records + # we'll ignore them at the moment + self.sdrtype = TYPE_UNKNOWN + pass + + def fru_decode(self, entry): + # table 43-7 FRU Device Locator + self.sdrtype = TYPE_FRU + self.fru_name = self.tlv_decode(entry[10], entry[11:]) + self.fru_number = entry[1] + + def association_decode(self, entry): + # table 43-4 Entity Associaition Record + #TODO(jbjohnso): actually represent this data + self.sdrtype = TYPE_UNKNOWN + + def compact_decode(self, entry): + # table 43-2 compact sensor record + self._common_decode(entry) + self.sensor_name = self.tlv_decode(entry[26], entry[27:]) + + def _common_decode(self, entry): + # compact and full are very similar + # this function handles the common aspects of compact and full + # offsets from spec, minus 6 + self.sensor_number = entry[2] + self.entity = ipmiconstants.entity_ids[entry[3]] + try: + self.sensor_type = ipmiconstants.sensor_type_codes[entry[7]] + except KeyError: + self.sensor_type = "UNKNOWN type " + str(entry[7]) + self.reading_type = entry[8] # table 42-1 + # 0: unspecified + # 1: generic threshold based + # 0x6f: discrete sensor-specific from table 42-3, sensor offsets + # all others per table 42-2, generic discrete + self.numeric_format = (entry[15] & 0b11000000) >> 6 + # the spec technically reserves numeric_format for compact sensor + # numeric, but common treatment won't break currently + # 0 - unsigned, 1 - 1s complement, 2 - 2s complement, 3 - ignore number + self.sensor_rate = sensor_rates[(entry[15] & 0b111000) >> 3] + self.unit_mod = "" + if (entry[15] & 0b110) == 0b10: # unit1 by unit2 + self.unit_mod = "/" + elif (entry[15] & 0b110) == 0b100: + # combine the units by multiplying, SI nomenclature is either spac + # or hyphen, so go with space + self.unit_mod = " " + self.percent = '' + if entry[15] & 1 == 1: + self.percent = '% ' + self.baseunit = unit_types[entry[16]] + self.modunit = unit_types[entry[17]] + self.unit_suffix = self.percent + self.baseunit + self.unit_mod + \ + self.modunit + + def full_decode(self, entry): + #offsets are table from spec, minus 6 + #TODO(jbjohnso): table 43-13, put in constants to interpret entry[3] + self._common_decode(entry) + # now must extract the formula data to transform values + # entry[18 to entry[24]. + # if not linear, must use get sensor reading factors + # TODO(jbjohnso): the various other values + self.sensor_name = self.tlv_decode(entry[42], entry[43:]) + self.linearization = entry[18] & 0b1111111 + if self.linearization <= 11: + # the enumuration of linear sensors goes to 11, + # static formula parameters are applicable, decode them + # if 0x70, then the sesor reading will have to get the + # factors on the fly. + # the formula could apply if we bother with nominal + # reading interpretation + self.decode_formula(entry) + + def decode_sensor_reading(self, reading): + numeric = None + output = { + 'name': self.sensor_name, + 'type': self.sensor_type, + } + if reading[1] & 0b100000: + output['unavailable'] = 1 + return SensorReading(output, self.unit_suffix) + if self.numeric_format == 2: + numeric = twos_complement(reading[0], 8) + elif self.numeric_format == 1: + numeric = ones_complement(reading[0], 8) + elif self.numeric_format == 0: + numeric = reading[0] + discrete = True + if numeric is not None: + lowerbound = numeric - (0.5 + (self.tolerance / 2.0)) + upperbound = numeric + (0.5 + (self.tolerance / 2.0)) + lowerbound = self.decode_value(lowerbound) + upperbound = self.decode_value(upperbound) + output['value'] = (lowerbound + upperbound) / 2.0 + output['imprecision'] = output['value'] - lowerbound + discrete = False + upper = 'upper' + lower = 'lower' + if self.linearization == 7: + # if the formula is 1/x, then the intuitive sense of upper and + # lower are backwards + upper = 'lower' + lower = 'upper' + output['states'] = [] + if not discrete: + output['health'] = const.Health.Ok + if reading[2] & 0b1: + output['health'] |= const.Health.Warning + output['states'].append(lower + " non-critical threshold") + if reading[2] & 0b10: + output['health'] |= const.Health.Critical + output['states'].append(lower + " critical threshold") + if reading[2] & 0b100: + output['health'] |= const.Health.Failed + output['states'].append(lower + " non-recoverable threshold") + if reading[2] & 0b1000: + output['health'] |= const.Health.Warning + output['states'].append(upper + " non-critical threshold") + if reading[2] & 0b10000: + output['health'] |= const.Health.Critical + output['states'].append(upper + " critical threshold") + if reading[2] & 0b100000: + output['health'] |= const.Health.Failed + output['states'].append(upper + " non-recoverable threshold") + return SensorReading(output, self.unit_suffix) + + def decode_value(self, value): + # Take the input value and return meaningful value + if self.linearization == 0x70: # direct calling code to get factors + #TODO(jbjohnso): implement get sensor reading factors support for + #non linear sensor + raise NotImplementedError("Need to do get sensor reading factors") + # time to compute the pre-linearization value. + decoded = float((value * self.m + self.b) * + (10 ** self.resultexponent)) + if self.linearization == 0: + return decoded + elif self.linearization == 1: + return math.log(decoded) + elif self.linearization == 2: + return math.log(decoded, 10) + elif self.linearization == 3: + return math.log(decoded, 2) + elif self.linearization == 4: + return math.exp(decoded) + elif self.linearization == 5: + return 10 ** decoded + elif self.linearization == 6: + return 2 ** decoded + elif self.linearization == 7: + return 1 / decoded + elif self.linearization == 8: + return decoded ** 2 + elif self.linearization == 9: + return decoded ** 3 + elif self.linearization == 10: + return math.sqrt(decoded) + elif self.linearization == 11: + return decoded ** (1.0/3) + else: + raise NotImplementedError + + def decode_formula(self, entry): + self.m = \ + twos_complement(entry[19] + ((entry[20] & 0b11000000) << 2), 10) + self.tolerance = entry[20] & 0b111111 + self.b = \ + twos_complement(entry[21] + ((entry[22] & 0b11000000) << 2), 10) + self.accuracy = (entry[22] & 0b111111) + \ + (entry[23] & 0b11110000) << 2 + self.accuracyexp = (entry[23] & 0b1100) >> 2 + self.direction = entry[23] & 0b11 + #0 = n/a, 1 = input, 2 = output + self.resultexponent = twos_complement((entry[24] & 0b11110000) >> 4, 4) + bexponent = twos_complement(entry[24] & 0b1111, 4) + # might as well do the math to 'b' now rather than wait for later + self.b = self.b * (10**bexponent) + + def tlv_decode(self, tlv, data): + # Per IPMI 'type/length byte format + type = (tlv & 0b11000000) >> 6 + if not len(data): + return "" + if type == 0: # Unicode per 43.15 in ipmi 2.0 spec + # the spec is not specific about encoding, assuming utf8 + return unicode(struct.pack("%dB" % len(data), *data), "utf_8") + elif type == 1: # BCD '+' + tmpl = "%02X" * len(data) + tstr = tmpl % tuple(data) + tstr = tstr.replace("A", " ").replace("B", "-").replace("C", ".") + return tstr.replace("D", ":").replace("E", ",").replace("F", "_") + elif type == 2: # 6 bit ascii, start at 0x20 and stop when out of bits + # the ordering is very peculiar and is best understood from + # IPMI SPEC "6-bit packed ascii example + tstr = "" + while len(data) >= 3: # the packing only works with 3 byte chunks + tstr += chr((data[0] & 0b111111) + 0x20) + tstr += chr(((data[1] & 0b1111) << 2) + + (data[0] >> 6) + 0x20) + tstr += chr(((data[2] & 0b11) << 4) + + (data[1] >> 4) + 0x20) + tstr += chr((data[2] >> 2) + 0x20) + return tstr + elif type == 3: # ACSII+LATIN1 + return struct.pack("%dB" % len(data), *data) + + +class SDR(object): + """Examine the state of sensors managed by a BMC + + Presents the data from sensor read commands as directed by the SDR in a + reasonable format. This module is used by the command module, and is not + intended for consumption by external code directly + + :param ipmicmd: A Command class object + """ + def __init__(self, ipmicmd): + self.ipmicmd = ipmicmd + self.sensors = {} + self.fru = {} + self.read_info() + + def read_info(self): + #first, we want to know the device id + rsp = self.ipmicmd.raw_command(netfn=6, command=1) + self.device_id = rsp['data'][0] + self.device_rev = rsp['data'][1] & 0b111 + # Going to ignore device available until get sdr command + # since that provides usefully distinct state and this does not + self.fw_major = rsp['data'][2] & 0b1111111 + self.fw_minor = "%02X" % rsp['data'][3] # BCD encoding, oddly enough + if rsp['data'][1] & 0b10000000: + # For lack of any system with 'device sdrs', raise an + # exception when they are encountered for now, implement or + # ignore later + raise NotImplementedError + self.ipmiversion = rsp['data'][4] # 51h = 1.5, 02h = 2.0 + self.mfg_id = rsp['data'][8] << 16 + rsp['data'][7] << 8 + \ + rsp['data'][6] + self.prod_id = rsp['data'][10] << 8 + rsp['data'][9] + if len(rsp['data']) > 11: + self.aux_fw = self.decode_aux(rsp['data'][11:15]) + self.get_sdr() + + def get_sdr(self): + rsp = self.ipmicmd.raw_command(netfn=0x0a, command=0x20) + if (rsp['data'][0] != 0x51): + # we only understand SDR version 51h, the only version defined + # at time of this writing + raise NotImplementedError + #NOTE(jbjohnso): we actually don't need to care about 'numrecords' + # since FFFF marks the end explicitly + #numrecords = (rsp['data'][2] << 8) + rsp['data'][1] + #NOTE(jbjohnso): don't care about 'free space' at the moment + #NOTE(jbjohnso): most recent timstamp data for add and erase could be + # handy to detect cache staleness, but for now will assume invariant + # over life of session + #NOTE(jbjohnso): not looking to support the various options in op + # support, ignore those for now, reservation if some BMCs can't read + # full SDR in one slurp + recid = 0 + rsvid = 0 # partial 'get sdr' will require this + offset = 0 + size = 0xff + while recid != 0xffff: # per 33.12 Get SDR command, 0xffff marks end + rqdata = [rsvid & 0xff, rsvid >> 8, + recid & 0xff, recid >> 8, + offset, size] + rsp = self.ipmicmd.raw_command(netfn=0x0a, command=0x23, + data=rqdata) + newrecid = (rsp['data'][1] << 8) + rsp['data'][0] + self.add_sdr(rsp['data'][2:]) + if newrecid == recid: + raise exc.BmcErrorException("Incorrect SDR record id from BMC") + recid = newrecid + + def get_sensor_numbers(self): + return self.sensors.iterkeys() + + def add_sdr(self, sdrbytes): + newent = SDREntry(sdrbytes) + if newent.sdrtype == TYPE_SENSOR: + id = newent.sensor_number + if id in self.sensors: + raise exc.BmcErrorException("Duplicate sensor number " + id) + self.sensors[id] = newent + elif newent.sdrtype == TYPE_FRU: + id = newent.fru_number + if id in self.fru: + raise exc.BmcErrorException("Duplicate FRU identifier " + id) + self.fru[id] = newent + + def decode_aux(self, auxdata): + # This is where manufacturers can add their own + # decode information + return "".join(hex(x) for x in auxdata) + +if __name__ == "__main__": # test code + import os + import sys + password = os.environ['IPMIPASSWORD'] + bmc = sys.argv[1] + user = sys.argv[2] + ipmicmd = ipmicmd.Command(bmc=bmc, userid=user, password=password) + sdr = SDR(ipmicmd) + for number in sdr.get_sensor_numbers(): + rsp = ipmicmd.raw_command(command=0x2d, netfn=4, data=(number,)) + if 'error' in rsp: + continue + reading = sdr.sensors[number].decode_sensor_reading(rsp['data']) + if reading is not None: + print repr(reading)