Implement consuming ipmi notifications from Ironic

This borrows heavily from the compute notifications code. The basic
idea is that there are few classes of samples that can be had
(Fan, Temperature, Voltage, Current). Within these are potentially
multiple individual sensors and values. In all cases the volume and
unit of a sample can always be extracted from the value of a
'Sensor Reading' key. The volume is cast to a float. If the value
is 'Disabled' the data is dropped.

Each subclass exists to allow flexible handling via stevedore, but
all activity is performed in the superclass.

The supplied good test data is the currently blessed sample from the
Ironic team. The bad test data tickles either skipping missing data or
producing warnings in the log.

Identifier and name construction is worth a close look because this
is an area of frequent discussion:

* resource_id is composed from node_uuid and sensor id
* meter name is based on the class name and is generic, only
  paired with a resource_id does it identify a specific sensor
* unit is parsed out of sensor data and mapped to SI units

Change-Id: Ie7ac92aebb45fc23d6134b4d6693f8a1e4d4ea40
Implements blueprint ironic-notifications
This commit is contained in:
Chris Dent 2014-07-08 16:06:49 +01:00
parent c1f4eaf312
commit 41e927622e
7 changed files with 1195 additions and 0 deletions

View File

@ -0,0 +1,176 @@
#
# Copyright 2014 Red Hat
#
# Author: Chris Dent <chdent@redhat.com>
#
# 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'

View File

@ -0,0 +1,785 @@
#
# Copyright 2014 Red Hat, Inc
#
# Author: Chris Dent <chdent@redhat.com>
#
# 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)',
},
}
}
}
}

View File

@ -0,0 +1,213 @@
#
# Copyright 2014 Red Hat, Inc
#
# Author: Chris Dent <chdent@redhat.com>
#
# 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]
)

View File

@ -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
=======================================================

View File

@ -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