summaryrefslogtreecommitdiff
path: root/ceilometer/ipmi
diff options
context:
space:
mode:
Diffstat (limited to 'ceilometer/ipmi')
-rw-r--r--ceilometer/ipmi/manager.py23
-rw-r--r--ceilometer/ipmi/notifications/__init__.py0
-rw-r--r--ceilometer/ipmi/notifications/ironic.py175
-rw-r--r--ceilometer/ipmi/pollsters/__init__.py0
-rw-r--r--ceilometer/ipmi/pollsters/node.py77
-rw-r--r--ceilometer/ipmi/pollsters/sensor.py97
6 files changed, 372 insertions, 0 deletions
diff --git a/ceilometer/ipmi/manager.py b/ceilometer/ipmi/manager.py
new file mode 100644
index 00000000..6fbb1ec6
--- /dev/null
+++ b/ceilometer/ipmi/manager.py
@@ -0,0 +1,23 @@
+# Copyright 2014 Intel
+#
+# Author: Zhai Edwin <edwin.zhai@intel.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.
+
+from ceilometer import agent
+
+
+class AgentManager(agent.AgentManager):
+
+ def __init__(self):
+ super(AgentManager, self).__init__('ipmi')
diff --git a/ceilometer/ipmi/notifications/__init__.py b/ceilometer/ipmi/notifications/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ceilometer/ipmi/notifications/__init__.py
diff --git a/ceilometer/ipmi/notifications/ironic.py b/ceilometer/ipmi/notifications/ironic.py
new file mode 100644
index 00000000..4e37eeaa
--- /dev/null
+++ b/ceilometer/ipmi/notifications/ironic.py
@@ -0,0 +1,175 @@
+#
+# 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',
+}
+
+
+def validate_reading(data):
+ """Some sensors read "Disabled"."""
+ return data != 'Disabled'
+
+
+def transform_id(data):
+ return data.lower().replace(' ', '_')
+
+
+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)
+
+
+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 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 []
+
+ def _package_payload(self, message, payload):
+ # NOTE(chdent): How much of the payload should we keep?
+ info = {'publisher_id': message['publisher_id'],
+ 'timestamp': message['payload']['timestamp'],
+ 'event_type': message['payload']['event_type'],
+ 'user_id': message['payload'].get('user_id'),
+ 'project_id': message['payload'].get('project_id'),
+ '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': 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 validate_reading(sensor_reading):
+ volume, unit = 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/ipmi/pollsters/__init__.py b/ceilometer/ipmi/pollsters/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ceilometer/ipmi/pollsters/__init__.py
diff --git a/ceilometer/ipmi/pollsters/node.py b/ceilometer/ipmi/pollsters/node.py
new file mode 100644
index 00000000..a04c3750
--- /dev/null
+++ b/ceilometer/ipmi/pollsters/node.py
@@ -0,0 +1,77 @@
+# Copyright 2014 Intel
+#
+# Author: Zhai Edwin <edwin.zhai@intel.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.
+
+import abc
+
+from oslo.config import cfg
+from oslo.utils import timeutils
+import six
+
+from ceilometer.ipmi.platform import intel_node_manager as node_manager
+from ceilometer import plugin
+from ceilometer import sample
+
+CONF = cfg.CONF
+CONF.import_opt('host', 'ceilometer.service')
+
+
+@six.add_metaclass(abc.ABCMeta)
+class _Base(plugin.PollsterBase):
+ def __init__(self):
+ self.nodemanager = node_manager.NodeManager()
+
+ @property
+ def default_discovery(self):
+ return None
+
+ @abc.abstractmethod
+ def read_data(self):
+ """Return data sample for IPMI."""
+
+ def get_samples(self, manager, cache, resources):
+ stats = self.read_data()
+
+ if stats:
+ data = node_manager._hex(stats["Current_value"])
+
+ yield sample.Sample(
+ name=self.NAME,
+ type=self.TYPE,
+ unit=self.UNIT,
+ volume=data,
+ user_id=None,
+ project_id=None,
+ resource_id=CONF.host,
+ timestamp=timeutils.utcnow().isoformat(),
+ resource_metadata=None)
+
+
+class TemperaturePollster(_Base):
+ NAME = "hardware.ipmi.node.temperature"
+ TYPE = sample.TYPE_GAUGE
+ UNIT = "C"
+
+ def read_data(self):
+ return self.nodemanager.read_temperature_all()
+
+
+class PowerPollster(_Base):
+ NAME = "hardware.ipmi.node.power"
+ TYPE = sample.TYPE_GAUGE
+ UNIT = "W"
+
+ def read_data(self):
+ return self.nodemanager.read_power_all()
diff --git a/ceilometer/ipmi/pollsters/sensor.py b/ceilometer/ipmi/pollsters/sensor.py
new file mode 100644
index 00000000..837aec3a
--- /dev/null
+++ b/ceilometer/ipmi/pollsters/sensor.py
@@ -0,0 +1,97 @@
+# Copyright 2014 Intel
+#
+# Author: Zhai Edwin <edwin.zhai@intel.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.
+
+from oslo.config import cfg
+from oslo.utils import timeutils
+
+from ceilometer.ipmi.notifications import ironic as parser
+from ceilometer.ipmi.platform import ipmi_sensor
+from ceilometer import plugin
+from ceilometer import sample
+
+CONF = cfg.CONF
+CONF.import_opt('host', 'ceilometer.service')
+
+
+class InvalidSensorData(ValueError):
+ pass
+
+
+class SensorPollster(plugin.PollsterBase):
+
+ METRIC = None
+
+ def __init__(self):
+ self.ipmi = ipmi_sensor.IPMISensor()
+
+ @property
+ def default_discovery(self):
+ return None
+
+ def _get_sensor_types(self, data, sensor_type):
+ try:
+ return (sensor_type_data for _, sensor_type_data
+ in data[sensor_type].items())
+ except KeyError:
+ return []
+
+ def get_samples(self, manager, cache, resources):
+ stats = self.ipmi.read_sensor_any(self.METRIC)
+
+ sensor_type_data = self._get_sensor_types(stats, self.METRIC)
+
+ for sensor_data in sensor_type_data:
+ try:
+ sensor_reading = sensor_data['Sensor Reading']
+ except KeyError:
+ raise InvalidSensorData("missing 'Sensor Reading'")
+
+ if not parser.validate_reading(sensor_reading):
+ continue
+
+ volume, unit = parser.parse_reading(sensor_reading)
+
+ resource_id = '%(host)s-%(sensor-id)s' % {
+ 'host': CONF.host,
+ 'sensor-id': parser.transform_id(sensor_data['Sensor ID'])
+ }
+
+ yield sample.Sample(
+ name='hardware.ipmi.%s' % self.METRIC.lower(),
+ type=sample.TYPE_GAUGE,
+ unit=unit,
+ volume=volume,
+ user_id=None,
+ project_id=None,
+ resource_id=resource_id,
+ timestamp=timeutils.utcnow().isoformat(),
+ resource_metadata=None)
+
+
+class TemperatureSensorPollster(SensorPollster):
+ METRIC = 'Temperature'
+
+
+class CurrentSensorPollster(SensorPollster):
+ METRIC = 'Current'
+
+
+class FanSensorPollster(SensorPollster):
+ METRIC = 'Fan'
+
+
+class VoltageSensorPollster(SensorPollster):
+ METRIC = 'Voltage'