# Copyright 2016 Lenovo # # 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. import struct import time import aiohmi.constants as pygconst import aiohmi.exceptions as pygexc try: range = xrange except NameError: pass try: buffer except NameError: buffer = memoryview psucfg_errors = { 0: 'Vendor mismatch', 1: 'Revision mismatch', 2: 'Processor missing', # e.g. pluggable CPU VRMs... 3: 'Insufficient power', 4: 'Voltage mismatch', } firmware_progress = { 0: 'Unspecified', 1: 'Memory initialization', 2: 'Disk initialization', 3: 'Non-primary Processor initialization', 4: 'User authentication', 5: 'In setup', 6: 'USB initialization', 7: 'PCI initialization', 8: 'Option ROM initialization', 9: 'Video initialization', 0xa: 'Cache initialization', 0xb: 'SMBus initialization', 0xc: 'Keyboard initialization', 0xd: 'Embedded controller initialization', 0xe: 'Docking station attachment', 0xf: 'Docking station enabled', 0x10: 'Docking station ejection', 0x11: 'Docking station disabled', 0x12: 'Waking OS', 0x13: 'Starting OS boot', 0x14: 'Baseboard initialization', 0x16: 'Floppy initialization', 0x17: 'Keyboard test', 0x18: 'Pointing device test', 0x19: 'Primary processor initialization', } firmware_errors = { 0: 'Unspecified', 1: 'No memory installed', 2: 'All memory failed', 3: 'Unrecoverable disk failure', 4: 'Unrecoverable board failure', 5: 'Unrecoverable diskette failure', 6: 'Unrecoverable storage controller failure', 7: 'Unrecoverable keyboard failure', # Keyboard error, press # any key to continue.. 8: 'Removable boot media not found', 9: 'Video adapter failure', 0xa: 'No video device', 0xb: 'Firmware corruption detected', 0xc: 'CPU voltage mismatch', 0xd: 'CPU speed mismatch', } auxlog_actions = { 0: 'entry added', 1: 'entry added (could not map to standard)', 2: 'entry added with corresponding standard events', 3: 'log cleared', 4: 'log disabled', 5: 'log enabled', } restart_causes = { 0: 'Unknown', 1: 'Remote request', 2: 'Reset button', 3: 'Power button', 4: 'Watchdog', 5: 'OEM', 6: 'Power restored', 7: 'Power restored', 8: 'Reset due to event', 9: 'Cycle due to event', 0xa: 'OS reset', 0xb: 'Timer wake', } slot_types = { 0: 'PCI', 1: 'Drive Array', 2: 'External connector', 3: 'Docking', 4: 'Other', 5: 'Entity ID', 6: 'AdvancedTCA', 7: 'Memory', 8: 'Fan', 9: 'PCIe', 10: 'SCSI', 11: 'SATA/SAS', 12: 'USB', } power_states = { 0: 'S0', 1: 'S1', 2: 'S2', 3: 'S3', 4: 'S4', 5: 'S5', 6: 'S4 or S5', 7: 'G3', 8: 'S1, S2, or S3', 9: 'G1', 0xa: 'S5', 0xb: 'on', 0xc: 'off', } watchdog_boot_phases = { 1: 'Firmware', 2: 'Firmware', 3: 'OS Load', 4: 'OS', 5: 'OEM', } version_changes = { 1: 'Device ID', 2: 'Management controller firmware', 3: 'Management controller revision', 4: 'Management conroller manufacturer', 5: 'IPMI version', 6: 'Management controller firmware', 7: 'Management controller boot block', 8: 'Management controller firmware', 9: 'System Firmware (UEFI/BIOS)', 0xa: 'SMBIOS', 0xb: 'OS', 0xc: 'OS Loader', 0xd: 'Diagnostics', 0xe: 'Management agent', 0xf: 'Management application', 0x10: 'Management middleware', 0x11: 'FPGA', 0x12: 'FRU', 0x13: 'FRU', 0x14: 'Equivalent FRU', 0x15: 'Updated FRU', 0x16: 'Older FRU', 0x17: 'Hardware (switch/jumper)', } fru_states = { 0: 'Normal', 1: 'Externally requested', 2: 'Latch', 3: 'Hot swap', 4: 'Internal action', 5: 'Lost communication', 6: 'Lost communication', 7: 'Unexpected removal', 8: 'Operator', 9: 'Unable to compute IPMB address', 0xa: 'Unexpected deactivation', } def decode_eventdata(sensor_type, offset, eventdata, event_consts, sdr): """Decode extra event data from an alert or log Provide a textual summary of eventdata per descriptions in Table 42-3 of the specification. This is for sensor specific offset events only. :param sensor_type: The sensor type number from the event :param offset: Sensor specific offset :param eventdata: The three bytes from the log or alert :param event_consts: event definition including severity. :param sdr: The sdr locator entry to help clarify how to parse data """ if sensor_type == 5 and offset == 4: # link loss, indicates which port return 'Port {0}'.format(eventdata[1]) elif sensor_type == 8 and offset == 6: # PSU cfg error errtype = eventdata[2] & 0b1111 return psucfg_errors.get(errtype, 'Unknown') elif sensor_type == 0xC6: return 'PSU Redundancy' elif sensor_type == 0xc and offset == 8: # Memory spare return 'Module {0}'.format(eventdata[2]) elif sensor_type == 0xf: if offset == 0: # firmware error return firmware_errors.get(eventdata[1], 'Unknown') elif offset in (1, 2): return firmware_progress.get(eventdata[1], 'Unknown') elif sensor_type == 0x10: if offset == 0: # Correctable error logging on a specific memory part return 'Module {0}'.format(eventdata[1]) elif offset == 1: return 'Reading type {0:02X}h, offset {1:02X}h'.format( eventdata[1], eventdata[2] & 0b1111) elif offset == 5: return '{0}%'.format(eventdata[2]) elif offset == 6: return 'Processor {0}'.format(eventdata[1]) elif sensor_type == 0x12: if offset == 3: action = (eventdata[1] & 0b1111000) >> 4 return auxlog_actions.get(action, 'Unknown') elif offset == 4: sysactions = [] if eventdata[1] & 0b1 << 5: sysactions.append('NMI') if eventdata[1] & 0b1 << 4: sysactions.append('OEM action') if eventdata[1] & 0b1 << 3: sysactions.append('Power Cycle') if eventdata[1] & 0b1 << 2: sysactions.append('Reset') if eventdata[1] & 0b1 << 1: sysactions.append('Power Down') if eventdata[1] & 0b1: sysactions.append('Alert') return ','.join(sysactions) elif offset == 5: # Clock change event, either before or after if eventdata[1] & 0b10000000: return 'After' else: return 'Before' elif sensor_type == 0x19 and offset == 0: return 'Requested {0} while {1}'.format(eventdata[1], eventdata[2]) elif sensor_type == 0x1d and offset == 7: return restart_causes.get(eventdata[1], 'Unknown') elif sensor_type == 0x21: return '{0} {1}'.format(slot_types.get(eventdata[1], 'Unknown'), eventdata[2]) elif sensor_type == 0x23: phase = eventdata[1] & 0b1111 return watchdog_boot_phases.get(phase, 'Unknown') elif sensor_type == 0x28: if offset == 4: return 'Sensor {0}'.format(eventdata[1]) elif offset == 5: islogical = (eventdata[1] & 0b10000000) if islogical: if eventdata[2] in sdr.fru: return sdr.fru[eventdata[2]].fru_name else: return 'FRU {0}'.format(eventdata[2]) elif sensor_type == 0x2a and offset == 3: return 'User {0}'.format(eventdata[1]) elif sensor_type == 0x2b: return version_changes.get(eventdata[1], 'Unknown') elif sensor_type == 0x2c: cause = (eventdata[1] & 0b11110000) >> 4 cause = fru_states.get(cause, 'Unknown') oldstate = eventdata[1] & 0b1111 if oldstate != offset: try: cause += '(change from {0})'.format( event_consts.sensor_type_offsets[0x2c][oldstate]['desc']) except KeyError: pass async def _fix_sel_time(records, ipmicmd): timefetched = False rsp = None while not timefetched: try: rsp = await ipmicmd.raw_command(netfn=0xa, command=0x48) timefetched = True except pygexc.IpmiException as pi: if pi.ipmicode == 0x81: continue raise # The specification declares an epoch and all that, but we really don't # care. We instead just focus on differences from the 'present' nowtime = struct.unpack_from(' lasttimestamp): # if the timestamp did something impossible, declare the rest # of history not meaningfully correctable correctionenabled = False newtimestamp = 0 continue newtimestamp = record['timecode'] trimindexes.append(index) elif ('event' in record and record['event'] == 'Clock time change' and record['event_data'] == 'Before'): if not correctionenabled: continue if newtimestamp: if record['timecode'] < 0x20000000: correctearly = True nowtime = correctednowtime # we want time that occurred before this point to get the delta # added to it to catch up correctednowtime += newtimestamp - record['timecode'] newtimestamp = 0 trimindexes.append(index) else: # clean up after potentially broken time sync pairs newtimestamp = 0 if record['timecode'] < 0x20000000: # uptime timestamp if not correctearly or not correctionenabled: correctednowtime = nowtime continue if (lasttimestamp is not None and record['timecode'] > lasttimestamp): # Time has gone backwards in pre-init, no hope for # accurate time correctearly = False correctionenabled = False correctednowtime = nowtime continue inpreinit = True lasttimestamp = record['timecode'] age = correctednowtime - record['timecode'] record['timestamp'] = time.strftime( '%Y-%m-%dT%H:%M:%S', time.localtime(time.time() - age)) else: # We are in 'normal' time, assume we cannot go to # pre-init time and do corrections unless time sync events # guide us in safely if (lasttimestamp is not None and record['timecode'] > lasttimestamp): # Time has gone backwards, without a clock sync # give up any attempt to correct from this point back... correctionenabled = False if inpreinit: inpreinit = False # We were in pre-init, now in real time, reset the # time correction factor to the last stored # 'wall clock' correction correctednowtime = nowtime correctearly = False lasttimestamp = record['timecode'] if not correctionenabled or correctednowtime < 0x20000000: # We can't correct time when the correction factor is # rooted in a pre-init timestamp, just convert record['timestamp'] = time.strftime( '%Y-%m-%dT%H:%M:%S', time.localtime( record['timecode'])) else: age = correctednowtime - record['timecode'] record['timestamp'] = time.strftime( '%Y-%m-%dT%H:%M:%S', time.localtime( time.time() - age)) for index in trimindexes: del records[index] class EventHandler(object): """IPMI Event Processor This class provides facilities for processing alerts and event log data. This can be used to aid in pulling historical event data from a BMC or as part of a trap handler to translate the traps into manageable data. :param sdr: An SDR object (per aiohmi.ipmi.sdr) matching the target BMC SDR :param ipmicmd: An ipmi command object to fetch data live """ @classmethod async def create(cls, sdr, ipmicmd): self = cls() self._sdr = sdr self._ipmicmd = ipmicmd self.event_consts = await ipmicmd.get_event_constants() return self def _populate_event(self, deassertion, event, event_data, event_type, sensor_type, sensorid): event['component_id'] = sensorid try: event['component'] = self._sdr.sensors[sensorid].name except KeyError: if sensorid == 0: event['component'] = None else: event['component'] = 'Sensor {0}'.format(sensorid) event['deassertion'] = deassertion event['event_data_bytes'] = event_data byte2type = (event_data[0] & 0b11000000) >> 6 byte3type = (event_data[0] & 0b110000) >> 4 if byte2type == 1: event['triggered_value'] = event_data[1] evtoffset = event_data[0] & 0b1111 event['event_type_byte'] = event_type if event_type <= 0xc: event['component_type_id'] = sensor_type event['event_id'] = '{0}.{1}'.format(event_type, evtoffset) # use generic offset decode for event description event['component_type'] = self.event_consts.sensor_type_codes.get( sensor_type, '') evreading = self.event_consts.generic_type_offsets.get( event_type, {}).get(evtoffset, {}) if event['deassertion']: event['event'] = evreading.get('deassertion_desc', '') event['severity'] = evreading.get( 'deassertion_severity', pygconst.Health.Ok) else: event['event'] = evreading.get('desc', '') event['severity'] = evreading.get( 'severity', pygconst.Health.Ok) elif event_type == 0x6f: event['component_type_id'] = sensor_type event['event_id'] = '{0}.{1}'.format(event_type, evtoffset) event['component_type'] = self.event_consts.sensor_type_codes.get( sensor_type, '') evreading = self.event_consts.sensor_type_offsets.get( sensor_type, {}).get(evtoffset, {}) if event['deassertion']: event['event'] = evreading.get('deassertion_desc', '') event['severity'] = evreading.get( 'deassertion_severity', pygconst.Health.Ok) else: event['event'] = evreading.get('desc', '') event['severity'] = evreading.get( 'severity', pygconst.Health.Ok) if event_type == 1: # threshold if byte3type == 1: event['threshold_value'] = event_data[2] if 3 in (byte2type, byte3type) or event_type == 0x6f: # sensor specific decode, see sdr module... # 2 - 0xc: generic discrete, 0x6f, sensor specific additionaldata = decode_eventdata( sensor_type, evtoffset, event_data, self.event_consts, self._sdr) if additionaldata: event['event_data'] = additionaldata async def decode_pet(self, specifictrap, petdata): if isinstance(specifictrap, int): specifictrap = struct.unpack('4B', struct.pack('>I', specifictrap)) if len(specifictrap) != 4: raise pygexc.InvalidParameterValue( 'specifictrap should be integer number or 4 byte array') specifictrap = bytearray(specifictrap) sensor_type = specifictrap[1] event_type = specifictrap[2] # Event Offset is in first event data byte, so no need to fetch it here # evtoffset = specifictrap[3] & 0b1111 deassertion = (specifictrap[3] & 0b10000000) == 0b10000000 # alertseverity = petdata[26] sensorid = '{0}.0'.format(petdata[28]) event_data = petdata[31:34] event = {} seqnum = struct.unpack_from('>H', buffer(petdata[16:18]))[0] ltimestamp = struct.unpack_from('>I', buffer(petdata[18:22]))[0] petack = bytearray(struct.pack('= 0xe0: # In this class of OEM message, all bytes are OEM, interpretation # is wholly left up to the OEM layer, using the OEM ID of the BMC event['oemdata'] = selentry[3:] await self._ipmicmd._oem.process_event(event, self._ipmicmd, selentry) if 'event_type_byte' in event: del event['event_type_byte'] if 'event_data_bytes' in event: del event['event_data_bytes'] return event async def _fetch_entries(self, ipmicmd, startat, targetlist, rsvid=0): curr = startat endat = curr while curr != 0xffff: endat = curr reqdata = bytearray(struct.pack('