diff --git a/ceilometer/hardware/notifications/__init__.py b/ceilometer/hardware/notifications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ceilometer/hardware/notifications/ipmi.py b/ceilometer/hardware/notifications/ipmi.py new file mode 100644 index 00000000..35fc7520 --- /dev/null +++ b/ceilometer/hardware/notifications/ipmi.py @@ -0,0 +1,176 @@ +# +# Copyright 2014 Red Hat +# +# Author: Chris Dent +# +# 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. +"""Converters for producing hardware sensor data sample messages from +notification events. +""" + +from oslo.config import cfg +from oslo import messaging + +from ceilometer.openstack.common import log +from ceilometer import plugin +from ceilometer import sample + +LOG = log.getLogger(__name__) + +OPTS = [ + cfg.StrOpt('ironic_exchange', + default='ironic', + help='Exchange name for Ironic notifications.'), +] + + +cfg.CONF.register_opts(OPTS) + + +# Map unit name to SI +UNIT_MAP = { + 'Watts': 'W', + 'Volts': 'V', +} + + +class InvalidSensorData(ValueError): + pass + + +class SensorNotification(plugin.NotificationBase): + """A generic class for extracting samples from sensor data notifications. + + A notification message can contain multiple samples from multiple + sensors, all with the same basic structure: the volume for the sample + is found as part of the value of a 'Sensor Reading' key. The unit + is in the same value. + + Subclasses exist solely to allow flexibility with stevedore configuration. + """ + + event_types = ['hardware.ipmi.*'] + metric = None + + @staticmethod + def get_targets(conf): + """oslo.messaging.TargetS for this this plugin.""" + return [messaging.Target(topic=topic, + exchange=conf.ironic_exchange) + for topic in conf.notification_topics] + + def _get_sample(self, message): + try: + return (payload for _, payload + in message['payload'][self.metric].items()) + except KeyError: + return [] + + @staticmethod + def _validate_reading(data): + """Some sensors read "Disabled".""" + return data != 'Disabled' + + @staticmethod + def _transform_id(data): + return data.lower().replace(' ', '_') + + @staticmethod + def _parse_reading(data): + try: + volume, unit = data.split(' ', 1) + unit = unit.rsplit(' ', 1)[-1] + return float(volume), UNIT_MAP.get(unit, unit) + except ValueError: + raise InvalidSensorData('unable to parse sensor reading: %s' % + data) + + def _package_payload(self, message, payload): + info = {} + info['publisher_id'] = message['publisher_id'] + info['timestamp'] = message['payload']['timestamp'] + info['event_type'] = message['payload']['event_type'] + info['user_id'] = message['payload'].get('user_id') + info['project_id'] = message['payload'].get('project_id') + # NOTE(chdent): How much of the payload should we keep? + info['payload'] = payload + return info + + def process_notification(self, message): + """Read and process a notification. + + The guts of a message are in dict value of a 'payload' key + which then itself has a payload key containing a dict of + multiple sensor readings. + + If expected keys in the payload are missing or values + are not in the expected form for transformations, + KeyError and ValueError are caught and the current + sensor payload is skipped. + """ + payloads = self._get_sample(message['payload']) + for payload in payloads: + try: + # Provide a fallback resource_id in case parts are missing. + resource_id = 'missing id' + try: + resource_id = '%(nodeid)s-%(sensorid)s' % { + 'nodeid': message['payload']['node_uuid'], + 'sensorid': self._transform_id(payload['Sensor ID']) + } + except KeyError as exc: + raise InvalidSensorData('missing key in payload: %s' % exc) + + info = self._package_payload(message, payload) + + try: + sensor_reading = info['payload']['Sensor Reading'] + except KeyError as exc: + raise InvalidSensorData( + "missing 'Sensor Reading' in payload" + ) + + if self._validate_reading(sensor_reading): + volume, unit = self._parse_reading(sensor_reading) + yield sample.Sample.from_notification( + name='hardware.ipmi.%s' % self.metric.lower(), + type=sample.TYPE_GAUGE, + unit=unit, + volume=volume, + resource_id=resource_id, + message=info, + user_id=info['user_id'], + project_id=info['project_id']) + + except InvalidSensorData as exc: + LOG.warn( + 'invalid sensor data for %(resource)s: %(error)s' % + dict(resource=resource_id, error=exc) + ) + continue + + +class TemperatureSensorNotification(SensorNotification): + metric = 'Temperature' + + +class CurrentSensorNotification(SensorNotification): + metric = 'Current' + + +class FanSensorNotification(SensorNotification): + metric = 'Fan' + + +class VoltageSensorNotification(SensorNotification): + metric = 'Voltage' diff --git a/ceilometer/tests/hardware/notifications/__init__.py b/ceilometer/tests/hardware/notifications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ceilometer/tests/hardware/notifications/ipmi_test_data.py b/ceilometer/tests/hardware/notifications/ipmi_test_data.py new file mode 100644 index 00000000..6cd42059 --- /dev/null +++ b/ceilometer/tests/hardware/notifications/ipmi_test_data.py @@ -0,0 +1,785 @@ +# +# Copyright 2014 Red Hat, Inc +# +# Author: Chris Dent +# +# 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. +"""Sample data for test_ipmi. + +This data is provided as a sample of the data expected from the ipmitool +driver in the Ironic project, which is the publisher of the notifications +being tested. +""" + + +SENSOR_DATA = { + 'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', + 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', + 'payload': { + 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', + 'timestamp': '20140223134852', + 'node_uuid': 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad', + 'event_type': 'hardware.ipmi.metrics.update', + 'payload': { + 'Temperature': { + 'DIMM GH VR Temp (0x3b)': { + 'Status': 'ok', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Sensor Reading': '26 (+/- 0.500) degrees C', + 'Entity ID': '20.6 (Power Module)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Positive Hysteresis': '4.000', + 'Assertion Events': '', + 'Upper non-critical': '95.000', + 'Event Message Control': 'Per-threshold', + 'Upper non-recoverable': '105.000', + 'Normal Maximum': '112.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '100.000', + 'Sensor ID': 'DIMM GH VR Temp (0x3b)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '16.000' + }, + 'CPU1 VR Temp (0x36)': { + 'Status': 'ok', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Sensor Reading': '32 (+/- 0.500) degrees C', + 'Entity ID': '20.1 (Power Module)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Positive Hysteresis': '4.000', + 'Assertion Events': '', + 'Upper non-critical': '95.000', + 'Event Message Control': 'Per-threshold', + 'Upper non-recoverable': '105.000', + 'Normal Maximum': '112.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '100.000', + 'Sensor ID': 'CPU1 VR Temp (0x36)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '16.000' + }, + 'DIMM EF VR Temp (0x3a)': { + 'Status': 'ok', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Sensor Reading': '26 (+/- 0.500) degrees C', + 'Entity ID': '20.5 (Power Module)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Positive Hysteresis': '4.000', + 'Assertion Events': '', + 'Upper non-critical': '95.000', + 'Event Message Control': 'Per-threshold', + 'Upper non-recoverable': '105.000', + 'Normal Maximum': '112.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '100.000', + 'Sensor ID': 'DIMM EF VR Temp (0x3a)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '16.000' + }, + 'CPU2 VR Temp (0x37)': { + 'Status': 'ok', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Sensor Reading': '31 (+/- 0.500) degrees C', + 'Entity ID': '20.2 (Power Module)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Positive Hysteresis': '4.000', + 'Assertion Events': '', + 'Upper non-critical': '95.000', + 'Event Message Control': 'Per-threshold', + 'Upper non-recoverable': '105.000', + 'Normal Maximum': '112.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '100.000', + 'Sensor ID': 'CPU2 VR Temp (0x37)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '16.000' + }, + 'Ambient Temp (0x32)': { + 'Status': 'ok', + 'Sensor Reading': '25 (+/- 0) degrees C', + 'Entity ID': '12.1 (Front Panel Board)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Event Message Control': 'Per-threshold', + 'Assertion Events': '', + 'Upper non-critical': '43.000', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Upper non-recoverable': '50.000', + 'Positive Hysteresis': '4.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '46.000', + 'Sensor ID': 'Ambient Temp (0x32)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '25.000' + }, + 'Mezz Card Temp (0x35)': { + 'Status': 'Disabled', + 'Sensor Reading': 'Disabled', + 'Entity ID': '44.1 (I/O Module)', + 'Event Message Control': 'Per-threshold', + 'Upper non-critical': '70.000', + 'Upper non-recoverable': '85.000', + 'Positive Hysteresis': '4.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '80.000', + 'Sensor ID': 'Mezz Card Temp (0x35)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '25.000' + }, + 'PCH Temp (0x3c)': { + 'Status': 'ok', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Sensor Reading': '46 (+/- 0.500) degrees C', + 'Entity ID': '45.1 (Processor/IO Module)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Positive Hysteresis': '4.000', + 'Assertion Events': '', + 'Upper non-critical': '93.000', + 'Event Message Control': 'Per-threshold', + 'Upper non-recoverable': '103.000', + 'Normal Maximum': '112.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '98.000', + 'Sensor ID': 'PCH Temp (0x3c)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '16.000' + }, + 'DIMM CD VR Temp (0x39)': { + 'Status': 'ok', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Sensor Reading': '27 (+/- 0.500) degrees C', + 'Entity ID': '20.4 (Power Module)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Positive Hysteresis': '4.000', + 'Assertion Events': '', + 'Upper non-critical': '95.000', + 'Event Message Control': 'Per-threshold', + 'Upper non-recoverable': '105.000', + 'Normal Maximum': '112.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '100.000', + 'Sensor ID': 'DIMM CD VR Temp (0x39)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '16.000' + }, + 'PCI Riser 2 Temp (0x34)': { + 'Status': 'ok', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Sensor Reading': '30 (+/- 0) degrees C', + 'Entity ID': '16.2 (System Internal Expansion Board)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Positive Hysteresis': '4.000', + 'Assertion Events': '', + 'Upper non-critical': '70.000', + 'Event Message Control': 'Per-threshold', + 'Upper non-recoverable': '85.000', + 'Normal Maximum': '112.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '80.000', + 'Sensor ID': 'PCI Riser 2 Temp (0x34)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '16.000' + }, + 'DIMM AB VR Temp (0x38)': { + 'Status': 'ok', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Sensor Reading': '28 (+/- 0.500) degrees C', + 'Entity ID': '20.3 (Power Module)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Positive Hysteresis': '4.000', + 'Assertion Events': '', + 'Upper non-critical': '95.000', + 'Event Message Control': 'Per-threshold', + 'Upper non-recoverable': '105.000', + 'Normal Maximum': '112.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '100.000', + 'Sensor ID': 'DIMM AB VR Temp (0x38)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '16.000' + }, + 'PCI Riser 1 Temp (0x33)': { + 'Status': 'ok', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Sensor Reading': '38 (+/- 0) degrees C', + 'Entity ID': '16.1 (System Internal Expansion Board)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Positive Hysteresis': '4.000', + 'Assertion Events': '', + 'Upper non-critical': '70.000', + 'Event Message Control': 'Per-threshold', + 'Upper non-recoverable': '85.000', + 'Normal Maximum': '112.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '80.000', + 'Sensor ID': 'PCI Riser 1 Temp (0x33)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '16.000' + }, + }, + 'Current': { + 'Avg Power (0x2e)': { + 'Status': 'ok', + 'Sensor Reading': '130 (+/- 0) Watts', + 'Entity ID': '21.0 (Power Management)', + 'Assertions Enabled': '', + 'Event Message Control': 'Per-threshold', + 'Readable Thresholds': 'No Thresholds', + 'Positive Hysteresis': 'Unspecified', + 'Sensor Type (Analog)': 'Current', + 'Negative Hysteresis': 'Unspecified', + 'Maximum sensor range': 'Unspecified', + 'Sensor ID': 'Avg Power (0x2e)', + 'Assertion Events': '', + 'Minimum sensor range': '2550.000', + 'Settable Thresholds': 'No Thresholds' + } + }, + 'Fan': { + 'Fan 4A Tach (0x46)': { + 'Status': 'ok', + 'Sensor Reading': '6900 (+/- 0) RPM', + 'Entity ID': '29.4 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2580.000', + 'Positive Hysteresis': '120.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '15300.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '120.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 4A Tach (0x46)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '4020.000' + }, + 'Fan 5A Tach (0x48)': { + 'Status': 'ok', + 'Sensor Reading': '7140 (+/- 0) RPM', + 'Entity ID': '29.5 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2580.000', + 'Positive Hysteresis': '120.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '15300.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '120.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 5A Tach (0x48)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '4020.000' + }, + 'Fan 3A Tach (0x44)': { + 'Status': 'ok', + 'Sensor Reading': '6900 (+/- 0) RPM', + 'Entity ID': '29.3 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2580.000', + 'Positive Hysteresis': '120.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '15300.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '120.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 3A Tach (0x44)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '4020.000' + }, + 'Fan 1A Tach (0x40)': { + 'Status': 'ok', + 'Sensor Reading': '6960 (+/- 0) RPM', + 'Entity ID': '29.1 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2580.000', + 'Positive Hysteresis': '120.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '15300.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '120.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 1A Tach (0x40)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '4020.000' + }, + 'Fan 3B Tach (0x45)': { + 'Status': 'ok', + 'Sensor Reading': '7104 (+/- 0) RPM', + 'Entity ID': '29.3 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2752.000', + 'Positive Hysteresis': '128.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '16320.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '128.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 3B Tach (0x45)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '3968.000' + }, + 'Fan 2A Tach (0x42)': { + 'Status': 'ok', + 'Sensor Reading': '7080 (+/- 0) RPM', + 'Entity ID': '29.2 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2580.000', + 'Positive Hysteresis': '120.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '15300.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '120.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 2A Tach (0x42)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '4020.000' + }, + 'Fan 4B Tach (0x47)': { + 'Status': 'ok', + 'Sensor Reading': '7488 (+/- 0) RPM', + 'Entity ID': '29.4 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2752.000', + 'Positive Hysteresis': '128.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '16320.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '128.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 4B Tach (0x47)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '3968.000' + }, + 'Fan 2B Tach (0x43)': { + 'Status': 'ok', + 'Sensor Reading': '7168 (+/- 0) RPM', + 'Entity ID': '29.2 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2752.000', + 'Positive Hysteresis': '128.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '16320.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '128.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 2B Tach (0x43)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '3968.000' + }, + 'Fan 5B Tach (0x49)': { + 'Status': 'ok', + 'Sensor Reading': '7296 (+/- 0) RPM', + 'Entity ID': '29.5 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2752.000', + 'Positive Hysteresis': '128.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '16320.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '128.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 5B Tach (0x49)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '3968.000' + }, + 'Fan 1B Tach (0x41)': { + 'Status': 'ok', + 'Sensor Reading': '7296 (+/- 0) RPM', + 'Entity ID': '29.1 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2752.000', + 'Positive Hysteresis': '128.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '16320.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '128.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 1B Tach (0x41)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '3968.000' + }, + 'Fan 6B Tach (0x4b)': { + 'Status': 'ok', + 'Sensor Reading': '7616 (+/- 0) RPM', + 'Entity ID': '29.6 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2752.000', + 'Positive Hysteresis': '128.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '16320.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '128.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 6B Tach (0x4b)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '3968.000' + }, + 'Fan 6A Tach (0x4a)': { + 'Status': 'ok', + 'Sensor Reading': '7080 (+/- 0) RPM', + 'Entity ID': '29.6 (Fan Device)', + 'Assertions Enabled': 'lcr-', + 'Normal Minimum': '2580.000', + 'Positive Hysteresis': '120.000', + 'Assertion Events': '', + 'Event Message Control': 'Per-threshold', + 'Normal Maximum': '15300.000', + 'Deassertions Enabled': 'lcr-', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '1920.000', + 'Negative Hysteresis': '120.000', + 'Threshold Read Mask': 'lcr', + 'Maximum sensor range': 'Unspecified', + 'Readable Thresholds': 'lcr', + 'Sensor ID': 'Fan 6A Tach (0x4a)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '4020.000' + } + }, + 'Voltage': { + 'Planar 12V (0x18)': { + 'Status': 'ok', + 'Sensor Reading': '12.312 (+/- 0) Volts', + 'Entity ID': '7.1 (System Board)', + 'Assertions Enabled': 'lcr- ucr+', + 'Event Message Control': 'Per-threshold', + 'Assertion Events': '', + 'Maximum sensor range': 'Unspecified', + 'Positive Hysteresis': '0.108', + 'Deassertions Enabled': 'lcr- ucr+', + 'Sensor Type (Analog)': 'Voltage', + 'Lower critical': '10.692', + 'Negative Hysteresis': '0.108', + 'Threshold Read Mask': 'lcr ucr', + 'Upper critical': '13.446', + 'Readable Thresholds': 'lcr ucr', + 'Sensor ID': 'Planar 12V (0x18)', + 'Settable Thresholds': 'lcr ucr', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '12.042' + }, + 'Planar 3.3V (0x16)': { + 'Status': 'ok', + 'Sensor Reading': '3.309 (+/- 0) Volts', + 'Entity ID': '7.1 (System Board)', + 'Assertions Enabled': 'lcr- ucr+', + 'Event Message Control': 'Per-threshold', + 'Assertion Events': '', + 'Maximum sensor range': 'Unspecified', + 'Positive Hysteresis': '0.028', + 'Deassertions Enabled': 'lcr- ucr+', + 'Sensor Type (Analog)': 'Voltage', + 'Lower critical': '3.039', + 'Negative Hysteresis': '0.028', + 'Threshold Read Mask': 'lcr ucr', + 'Upper critical': '3.564', + 'Readable Thresholds': 'lcr ucr', + 'Sensor ID': 'Planar 3.3V (0x16)', + 'Settable Thresholds': 'lcr ucr', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '3.309' + }, + 'Planar VBAT (0x1c)': { + 'Status': 'ok', + 'Sensor Reading': '3.137 (+/- 0) Volts', + 'Entity ID': '7.1 (System Board)', + 'Assertions Enabled': 'lnc- lcr-', + 'Event Message Control': 'Per-threshold', + 'Assertion Events': '', + 'Readable Thresholds': 'lcr lnc', + 'Positive Hysteresis': '0.025', + 'Deassertions Enabled': 'lnc- lcr-', + 'Sensor Type (Analog)': 'Voltage', + 'Lower critical': '2.095', + 'Negative Hysteresis': '0.025', + 'Lower non-critical': '2.248', + 'Maximum sensor range': 'Unspecified', + 'Sensor ID': 'Planar VBAT (0x1c)', + 'Settable Thresholds': 'lcr lnc', + 'Threshold Read Mask': 'lcr lnc', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '3.010' + }, + 'Planar 5V (0x17)': { + 'Status': 'ok', + 'Sensor Reading': '5.062 (+/- 0) Volts', + 'Entity ID': '7.1 (System Board)', + 'Assertions Enabled': 'lcr- ucr+', + 'Event Message Control': 'Per-threshold', + 'Assertion Events': '', + 'Maximum sensor range': 'Unspecified', + 'Positive Hysteresis': '0.045', + 'Deassertions Enabled': 'lcr- ucr+', + 'Sensor Type (Analog)': 'Voltage', + 'Lower critical': '4.475', + 'Negative Hysteresis': '0.045', + 'Threshold Read Mask': 'lcr ucr', + 'Upper critical': '5.582', + 'Readable Thresholds': 'lcr ucr', + 'Sensor ID': 'Planar 5V (0x17)', + 'Settable Thresholds': 'lcr ucr', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '4.995' + } + } + } + } +} + + +EMPTY_PAYLOAD = { + 'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', + 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', + 'payload': { + 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', + 'timestamp': '20140223134852', + 'node_uuid': 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad', + 'event_type': 'hardware.ipmi.metrics.update', + 'payload': { + } + } +} + + +MISSING_SENSOR = { + 'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', + 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', + 'payload': { + 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', + 'timestamp': '20140223134852', + 'node_uuid': 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad', + 'event_type': 'hardware.ipmi.metrics.update', + 'payload': { + 'Temperature': { + 'PCI Riser 1 Temp (0x33)': { + 'Status': 'ok', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Entity ID': '16.1 (System Internal Expansion Board)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Positive Hysteresis': '4.000', + 'Assertion Events': '', + 'Upper non-critical': '70.000', + 'Event Message Control': 'Per-threshold', + 'Upper non-recoverable': '85.000', + 'Normal Maximum': '112.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '80.000', + 'Sensor ID': 'PCI Riser 1 Temp (0x33)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '16.000' + }, + } + } + } +} + + +BAD_SENSOR = { + 'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', + 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', + 'payload': { + 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', + 'timestamp': '20140223134852', + 'node_uuid': 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad', + 'event_type': 'hardware.ipmi.metrics.update', + 'payload': { + 'Temperature': { + 'PCI Riser 1 Temp (0x33)': { + 'Status': 'ok', + 'Deassertions Enabled': 'unc+ ucr+ unr+', + 'Sensor Reading': 'some bad stuff', + 'Entity ID': '16.1 (System Internal Expansion Board)', + 'Assertions Enabled': 'unc+ ucr+ unr+', + 'Positive Hysteresis': '4.000', + 'Assertion Events': '', + 'Upper non-critical': '70.000', + 'Event Message Control': 'Per-threshold', + 'Upper non-recoverable': '85.000', + 'Normal Maximum': '112.000', + 'Maximum sensor range': 'Unspecified', + 'Sensor Type (Analog)': 'Temperature', + 'Readable Thresholds': 'unc ucr unr', + 'Negative Hysteresis': 'Unspecified', + 'Threshold Read Mask': 'unc ucr unr', + 'Upper critical': '80.000', + 'Sensor ID': 'PCI Riser 1 Temp (0x33)', + 'Settable Thresholds': '', + 'Minimum sensor range': 'Unspecified', + 'Nominal Reading': '16.000' + }, + } + } + } +} + + +NO_SENSOR_ID = { + 'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', + 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', + 'payload': { + 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', + 'timestamp': '20140223134852', + 'node_uuid': 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad', + 'event_type': 'hardware.ipmi.metrics.update', + 'payload': { + 'Temperature': { + 'PCI Riser 1 Temp (0x33)': { + 'Sensor Reading': '26 C', + }, + } + } + } +} + + +NO_NODE_ID = { + 'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', + 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', + 'payload': { + 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', + 'timestamp': '20140223134852', + 'event_type': 'hardware.ipmi.metrics.update', + 'payload': { + 'Temperature': { + 'PCI Riser 1 Temp (0x33)': { + 'Sensor Reading': '26 C', + 'Sensor ID': 'PCI Riser 1 Temp (0x33)', + }, + } + } + } +} diff --git a/ceilometer/tests/hardware/notifications/test_ipmi.py b/ceilometer/tests/hardware/notifications/test_ipmi.py new file mode 100644 index 00000000..c1d7070c --- /dev/null +++ b/ceilometer/tests/hardware/notifications/test_ipmi.py @@ -0,0 +1,213 @@ +# +# Copyright 2014 Red Hat, Inc +# +# Author: Chris Dent +# +# 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. +"""Tests for producing IPMI sample messages from notification events. +""" + +import mock + +from ceilometer.hardware.notifications import ipmi +from ceilometer.openstack.common import test +from ceilometer import sample +from ceilometer.tests.hardware.notifications import ipmi_test_data + + +class TestNotifications(test.BaseTestCase): + + def test_ipmi_temperature_notification(self): + """Test IPMI Temperature sensor data. + + Based on the above test data the expected sample for a single + temperature reading has:: + + * a resource_id composed from the node_uuid Sensor ID + * a name composed from 'hardware.ipmi.' and 'temperature' + * a volume from the first chunk of the Sensor Reading + * a unit from the last chunk of the Sensor Reading + * some readings are skipped if the value is 'Disabled' + """ + processor = ipmi.TemperatureSensorNotification(None) + counters = dict([(counter.resource_id, counter) for counter in + processor.process_notification( + ipmi_test_data.SENSOR_DATA)]) + + self.assertEqual(10, len(counters), + 'expected 10 temperature readings') + resource_id = ( + 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-dimm_gh_vr_temp_(0x3b)' + ) + test_counter = counters[resource_id] + self.assertEqual(26.0, test_counter.volume) + self.assertEqual('C', test_counter.unit) + self.assertEqual(sample.TYPE_GAUGE, test_counter.type) + self.assertEqual('hardware.ipmi.temperature', test_counter.name) + self.assertEqual('hardware.ipmi.metrics.update', + test_counter.resource_metadata['event_type']) + + def test_ipmi_current_notification(self): + """Test IPMI Current sensor data. + + A single current reading is effectively the same as temperature, + modulo "current". + """ + processor = ipmi.CurrentSensorNotification(None) + counters = dict([(counter.resource_id, counter) for counter in + processor.process_notification( + ipmi_test_data.SENSOR_DATA)]) + + self.assertEqual(1, len(counters), 'expected 1 current reading') + resource_id = ( + 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-avg_power_(0x2e)' + ) + test_counter = counters[resource_id] + self.assertEqual(130.0, test_counter.volume) + self.assertEqual('W', test_counter.unit) + self.assertEqual(sample.TYPE_GAUGE, test_counter.type) + self.assertEqual('hardware.ipmi.current', test_counter.name) + + def test_ipmi_fan_notification(self): + """Test IPMI Fan sensor data. + + A single fan reading is effectively the same as temperature, + modulo "fan". + """ + processor = ipmi.FanSensorNotification(None) + counters = dict([(counter.resource_id, counter) for counter in + processor.process_notification( + ipmi_test_data.SENSOR_DATA)]) + + self.assertEqual(12, len(counters), 'expected 12 fan readings') + resource_id = ( + 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-fan_4a_tach_(0x46)' + ) + test_counter = counters[resource_id] + self.assertEqual(6900.0, test_counter.volume) + self.assertEqual('RPM', test_counter.unit) + self.assertEqual(sample.TYPE_GAUGE, test_counter.type) + self.assertEqual('hardware.ipmi.fan', test_counter.name) + + def test_ipmi_voltage_notification(self): + """Test IPMI Voltage sensor data. + + A single voltage reading is effectively the same as temperature, + modulo "voltage". + """ + processor = ipmi.VoltageSensorNotification(None) + counters = dict([(counter.resource_id, counter) for counter in + processor.process_notification( + ipmi_test_data.SENSOR_DATA)]) + + self.assertEqual(4, len(counters), 'expected 4 volate readings') + resource_id = ( + 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-planar_vbat_(0x1c)' + ) + test_counter = counters[resource_id] + self.assertEqual(3.137, test_counter.volume) + self.assertEqual('V', test_counter.unit) + self.assertEqual(sample.TYPE_GAUGE, test_counter.type) + self.assertEqual('hardware.ipmi.voltage', test_counter.name) + + def test_disabed_skips_metric(self): + """Test that a meter which a disabled volume is skipped.""" + processor = ipmi.TemperatureSensorNotification(None) + counters = dict([(counter.resource_id, counter) for counter in + processor.process_notification( + ipmi_test_data.SENSOR_DATA)]) + + self.assertEqual(10, len(counters), + 'expected 10 temperature readings') + + resource_id = ( + 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-mezz_card_temp_(0x35)' + ) + + self.assertNotIn(resource_id, counters) + + def test_empty_payload_no_metrics_success(self): + processor = ipmi.TemperatureSensorNotification(None) + counters = dict([(counter.resource_id, counter) for counter in + processor.process_notification( + ipmi_test_data.EMPTY_PAYLOAD)]) + + self.assertEqual(0, len(counters), 'expected 0 readings') + + @mock.patch('ceilometer.hardware.notifications.ipmi.LOG') + def test_missing_sensor_data(self, mylog): + processor = ipmi.TemperatureSensorNotification(None) + + messages = [] + mylog.warn = lambda *args: messages.extend(args) + + list(processor.process_notification(ipmi_test_data.MISSING_SENSOR)) + + self.assertEqual( + 'invalid sensor data for ' + 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-pci_riser_1_temp_(0x33): ' + "missing 'Sensor Reading' in payload", + messages[0] + ) + + @mock.patch('ceilometer.hardware.notifications.ipmi.LOG') + def test_sensor_data_malformed(self, mylog): + processor = ipmi.TemperatureSensorNotification(None) + + messages = [] + mylog.warn = lambda *args: messages.extend(args) + + list(processor.process_notification(ipmi_test_data.BAD_SENSOR)) + + self.assertEqual( + 'invalid sensor data for ' + 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-pci_riser_1_temp_(0x33): ' + 'unable to parse sensor reading: some bad stuff', + messages[0] + ) + + @mock.patch('ceilometer.hardware.notifications.ipmi.LOG') + def test_missing_node_uuid(self, mylog): + """Test for desired error message when 'node_uuid' missing. + + Presumably this will never happen given the way the data + is created, but better defensive than dead. + """ + processor = ipmi.TemperatureSensorNotification(None) + + messages = [] + mylog.warn = lambda *args: messages.extend(args) + + list(processor.process_notification(ipmi_test_data.NO_NODE_ID)) + + self.assertEqual( + 'invalid sensor data for missing id: missing key in payload: ' + "'node_uuid'", + messages[0] + ) + + @mock.patch('ceilometer.hardware.notifications.ipmi.LOG') + def test_missing_sensor_id(self, mylog): + """Test for desired error message when 'Sensor ID' missing.""" + processor = ipmi.TemperatureSensorNotification(None) + + messages = [] + mylog.warn = lambda *args: messages.extend(args) + + list(processor.process_notification(ipmi_test_data.NO_SENSOR_ID)) + + self.assertEqual( + 'invalid sensor data for missing id: missing key in payload: ' + "'Sensor ID'", + messages[0] + ) diff --git a/doc/source/measurements.rst b/doc/source/measurements.rst index 23ca4b20..776a5661 100644 --- a/doc/source/measurements.rst +++ b/doc/source/measurements.rst @@ -281,6 +281,23 @@ network.services.lb.incoming.bytes Cumulative B pool ID p network.services.lb.outgoing.bytes Cumulative B pool ID pollster Number of outgoing Bytes ======================================= ========== ========== ========== ========= ============================== +Ironic Hardware IPMI Sensor Data +================================ + +IPMI sensor data is not available by default in Ironic. To enable these meters +see the `Ironic Installation Guide`_. + +.. _Ironic Installation Guide: http://docs.openstack.org/developer/ironic/deploy/install-guide.html + +============================= ========== ====== ============== ============ ========================== +Meter Type Unit Resource Origin Note +============================= ========== ====== ============== ============ ========================== +hardware.ipmi.fan Gauge RPM fan sensor notification Fan RPM +hardware.ipmi.temperature Gauge C temp sensor notification Sensor Temperature Reading +hardware.ipmi.current Gauge W current sensor notification Sensor Current Reading +hardware.ipmi.voltage Gauge V voltage sensor notification Sensor Voltage Reading +============================= ========== ====== ============== ============ ========================== + Dynamically retrieving the Meters via ceilometer client ======================================================= diff --git a/setup.cfg b/setup.cfg index 8b85e9b5..c16d67eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,6 +66,10 @@ ceilometer.notification = http.response = ceilometer.middleware:HTTPResponse stack_crud = ceilometer.orchestration.notifications:StackCRUD profiler = ceilometer.profiler.notifications:ProfilerNotifications + hardware.ipmi.temperature = ceilometer.hardware.notifications.ipmi:TemperatureSensorNotification + hardware.ipmi.voltage = ceilometer.hardware.notifications.ipmi:VoltageSensorNotification + hardware.ipmi.current = ceilometer.hardware.notifications.ipmi:CurrentSensorNotification + hardware.ipmi.fan = ceilometer.hardware.notifications.ipmi:FanSensorNotification ceilometer.discover = local_instances = ceilometer.compute.discovery:InstanceDiscovery