Merge "Implements send-data-to-ceilometer"

This commit is contained in:
Jenkins 2014-08-01 12:29:38 +00:00 committed by Gerrit Code Review
commit 00894bc755
12 changed files with 570 additions and 0 deletions

View File

@ -351,3 +351,22 @@ http://ipmitool.sourceforge.net/
Note that certain distros, notably Mac OS X and SLES, install ``openipmi``
instead of ``ipmitool`` by default. THIS DRIVER IS NOT COMPATIBLE WITH
``openipmi`` AS IT RELIES ON ERROR HANDLING OPTIONS NOT PROVIDED BY THIS TOOL.
Ironic supports sending IPMI sensor data to Ceilometer with pxe_ipmitool
driver. By default, support for sending IPMI sensor data to Ceilometer is
disabled. If you want to enable it set the following options in the
``conductor`` section of ``ironic.conf``:
* notification_driver=messaging
* send_sensor_data=true
If you want to customize the sensor types which will be sent to Ceilometer,
change the ``send_sensor_data_types`` option. For example, the below settings
will send Temperature,Fan,Voltage these three sensor types data to Ceilometer:
* send_sensor_data_types=Temperature,Fan,Voltage
Else we use default value 'All' for all the sensor types which supported by
Ceilometer, they are:
* Temperature,Fan,Voltage,Current

View File

@ -540,6 +540,19 @@
# Seconds to sleep between node lock attempts. (integer value)
#node_locked_retry_interval=1
# Enable sending sensor data message via the notification bus
# (boolean value)
#send_sensor_data=false
# Seconds between conductor sending sensor data message to
# ceilometer via the notification bus. (integer value)
#send_sensor_data_interval=600
# List of comma separated metric types which need to be sent
# to Ceilometer. The default value, "ALL", is a special value
# meaning send all the sensor data. (list value)
#send_sensor_data_types=ALL
[console]

View File

@ -398,3 +398,13 @@ class PasswordFileFailedToCreate(IronicException):
class IloOperationError(IronicException):
message = _("%(operation)s failed, error: %(error)s")
class FailedToGetSensorData(IronicException):
message = _("Failed to get sensor data for node %(node)s. "
"Error: %(error)s")
class FailedToParseSensorData(IronicException):
message = _("Failed to parse sensor data for node %(node)s. "
"Error: %(error)s")

View File

@ -42,6 +42,7 @@ a change, etc.
"""
import collections
import datetime
import threading
import eventlet
@ -55,7 +56,9 @@ from ironic.common import exception
from ironic.common import hash_ring as hash
from ironic.common import i18n
from ironic.common import neutron
from ironic.common import rpc
from ironic.common import states
from ironic.common import utils as ironic_utils
from ironic.conductor import task_manager
from ironic.conductor import utils
from ironic.db import api as dbapi
@ -123,6 +126,20 @@ conductor_opts = [
cfg.IntOpt('node_locked_retry_interval',
default=1,
help='Seconds to sleep between node lock attempts.'),
cfg.BoolOpt('send_sensor_data',
default=False,
help='Enable sending sensor data message via the '
'notification bus'),
cfg.IntOpt('send_sensor_data_interval',
default=600,
help='Seconds between conductor sending sensor data message'
' to ceilometer via the notification bus.'),
cfg.ListOpt('send_sensor_data_types',
default=['ALL'],
help='List of comma separated metric types which need to be'
' sent to Ceilometer. The default value, "ALL", is a '
'special value meaning send all the sensor data.'
),
]
CONF = cfg.CONF
@ -143,6 +160,7 @@ class ConductorManager(periodic_task.PeriodicTasks):
self.host = host
self.topic = topic
self.power_state_sync_count = collections.defaultdict(int)
self.notifier = rpc.get_notifier()
def _get_driver(self, driver_name):
"""Get the driver.
@ -1048,3 +1066,68 @@ class ConductorManager(periodic_task.PeriodicTasks):
driver_name)
driver = self._get_driver(driver_name)
return driver.get_properties()
@periodic_task.periodic_task(
spacing=CONF.conductor.send_sensor_data_interval)
def _send_sensor_data(self, context):
# do nothing if send_sensor_data option is False
if not CONF.conductor.send_sensor_data:
return
filters = {'associated': True}
columns = ['uuid', 'driver', 'instance_uuid']
node_list = self.dbapi.get_nodeinfo_list(columns=columns,
filters=filters)
for (node_uuid, driver, instance_uuid) in node_list:
# only handle the nodes mapped to this conductor
if not self._mapped_to_this_conductor(node_uuid, driver):
continue
# populate the message which will be sent to ceilometer
message = {'message_id': ironic_utils.generate_uuid(),
'instance_uuid': instance_uuid,
'node_uuid': node_uuid,
'timestamp': datetime.datetime.utcnow(),
'event_type': 'hardware.ipmi.metrics.update'}
try:
with task_manager.acquire(context, node_uuid, shared=True) \
as task:
sensors_data = task.driver.management.get_sensors_data(
task)
except NotImplementedError:
LOG.warn(_LW('get_sensors_data is not implemented for driver'
' %(driver)s, node_uuid is %(node)s'),
{'node': node_uuid, 'driver': driver})
except exception.FailedToParseSensorData as fps:
LOG.warn(_LW("During get_sensors_data, could not parse "
"sensor data for node %(node)s. Error: %(err)s."),
{'node': node_uuid, 'err': str(fps)})
except exception.FailedToGetSensorData as fgs:
LOG.warn(_LW("During get_sensors_data, could not get "
"sensor data for node %(node)s. Error: %(err)s."),
{'node': node_uuid, 'err': str(fgs)})
except exception.NodeNotFound:
LOG.warn(_LW("During send_sensor_data, node %(node)s was not "
"found and presumed deleted by another process."),
{'node': node_uuid})
except Exception as e:
LOG.warn(_LW("Failed to get sensor data for node %(node)s. "
"Error: %(error)s"), {'node': node_uuid, 'error': str(e)})
else:
message['payload'] = self._filter_out_unsupported_types(
sensors_data)
if message['payload']:
self.notifier.info(context, "hardware.ipmi.metrics",
message)
def _filter_out_unsupported_types(self, sensors_data):
# support the CONF.send_sensor_data_types sensor types only
allowed = set(x.lower() for x in CONF.conductor.send_sensor_data_types)
if 'all' in allowed:
return sensors_data
return dict((sensor_type, sensor_value) for (sensor_type, sensor_value)
in sensors_data.items() if sensor_type.lower() in allowed)

View File

@ -463,3 +463,40 @@ class ManagementInterface(object):
future boots or not, None if it is unknown.
"""
@abc.abstractmethod
def get_sensors_data(self, task):
"""Get sensors data method.
:param task: a TaskManager instance.
:raises: FailedToGetSensorData when getting the sensor data fails.
:raises: FailedToParseSensorData when parsing sensor data fails.
:returns: returns a consistent format dict of sensor data grouped by
sensor type, which can be processed by Ceilometer.
eg, {
'Sensor Type 1': {
'Sensor ID 1': {
'Sensor Reading': 'current value',
'key1': 'value1',
'key2': 'value2'
},
'Sensor ID 2': {
'Sensor Reading': 'current value',
'key1': 'value1',
'key2': 'value2'
}
},
'Sensor Type 2': {
'Sensor ID 3': {
'Sensor Reading': 'current value',
'key1': 'value1',
'key2': 'value2'
},
'Sensor ID 4': {
'Sensor Reading': 'current value',
'key1': 'value1',
'key2': 'value2'
}
}
}
"""

View File

@ -182,3 +182,6 @@ class FakeManagement(base.ManagementInterface):
def get_boot_device(self, task):
return {'boot_device': boot_devices.PXE, 'persistent': False}
def get_sensors_data(self, task):
return {}

View File

@ -388,3 +388,13 @@ class NativeIPMIManagement(base.ManagementInterface):
_BOOT_DEVICES_MAP.items()
if hdev == bootdev), None)
return response
def get_sensors_data(self, task):
"""Get sensors data.
Not implemented by this driver.
:param task: a TaskManager instance.
"""
raise NotImplementedError()

View File

@ -374,6 +374,76 @@ def _power_status(driver_info):
return states.ERROR
def _process_sensor(sensor_data):
sensor_data_fields = sensor_data.split('\n')
sensor_data_dict = {}
for field in sensor_data_fields:
if not field:
continue
kv_value = field.split(':')
if len(kv_value) != 2:
continue
sensor_data_dict[kv_value[0].strip()] = kv_value[1].strip()
return sensor_data_dict
def _get_sensor_type(node, sensor_data_dict):
# Have only three sensor type name IDs: 'Sensor Type (Analog)'
# 'Sensor Type (Discrete)' and 'Sensor Type (Threshold)'
for key in ('Sensor Type (Analog)', 'Sensor Type (Discrete)',
'Sensor Type (Threshold)'):
try:
return sensor_data_dict[key].split(' ', 1)[0]
except KeyError:
continue
raise exception.FailedToParseSensorData(
node=node.uuid,
error=(_("parse ipmi sensor data failed, unknown sensor type"
" data: %(sensors_data)s"), {'sensors_data': sensor_data_dict}))
def _parse_ipmi_sensors_data(node, sensors_data):
"""Parse the IPMI sensors data and format to the dict grouping by type.
We run 'ipmitool' command with 'sdr -v' options, which can return sensor
details in human-readable format, we need to format them to JSON string
dict-based data for Ceilometer Collector which can be sent it as payload
out via notification bus and consumed by Ceilometer Collector.
:param sensors_data: the sensor data returned by ipmitool command.
:returns: the sensor data with JSON format, grouped by sensor type.
:raises: FailedToParseSensorData when error encountered during parsing.
"""
sensors_data_dict = {}
if not sensors_data:
return sensors_data_dict
sensors_data_array = sensors_data.split('\n\n')
for sensor_data in sensors_data_array:
sensor_data_dict = _process_sensor(sensor_data)
if not sensor_data_dict:
continue
sensor_type = _get_sensor_type(node, sensor_data_dict)
# ignore the sensors which has no current 'Sensor Reading' data
if 'Sensor Reading' in sensor_data_dict:
sensors_data_dict.setdefault(sensor_type,
{})[sensor_data_dict['Sensor ID']] = sensor_data_dict
# get nothing, no valid sensor data
if not sensors_data_dict:
raise exception.FailedToParseSensorData(
node=node.uuid,
error=(_("parse ipmi sensor data failed, get nothing with input"
" data: %(sensors_data)s") % {'sensors_data': sensors_data}))
return sensors_data_dict
class IPMIPower(base.PowerInterface):
def __init__(self):
@ -464,6 +534,15 @@ class IPMIManagement(base.ManagementInterface):
def get_properties(self):
return COMMON_PROPERTIES
def __init__(self):
try:
check_timing_support()
except OSError:
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason="Unable to locate usable ipmitool command in "
"the system path when checking ipmitool version")
def validate(self, task):
"""Check that 'driver_info' contains IPMI credentials.
@ -569,6 +648,28 @@ class IPMIManagement(base.ManagementInterface):
response['persistent'] = 'Options apply to all future boots' in out
return response
def get_sensors_data(self, task):
"""Get sensors data.
:param task: a TaskManager instance.
:raises: FailedToGetSensorData when getting the sensor data fails.
:raises: FailedToParseSensorData when parsing sensor data fails.
:raises: InvalidParameterValue if required ipmi parameters are missing
:returns: returns a dict of sensor data group by sensor type.
"""
driver_info = _parse_driver_info(task.node)
# with '-v' option, we can get the entire sensor data including the
# extended sensor informations
cmd = "sdr -v"
try:
out, err = _exec_ipmitool(driver_info, cmd)
except processutils.ProcessExecutionError as pee:
raise exception.FailedToGetSensorData(node=task.node.uuid,
error=str(pee))
return _parse_ipmi_sensors_data(task.node, out)
class VendorPassthru(base.VendorInterface):

View File

@ -559,3 +559,12 @@ class Management(base.ManagementInterface):
# doesn't expose a method to get the boot device, update it once
# it's implemented.
return {'boot_device': None, 'persistent': None}
def get_sensors_data(self, task):
"""Get sensors data method.
Not implemented by this driver.
:param task: a TaskManager instance.
"""
raise NotImplementedError()

View File

@ -633,3 +633,13 @@ class SSHManagement(base.ManagementInterface):
"operation"),
{'node': node.uuid, 'vtype': driver_info['virt_type']})
return response
def get_sensors_data(self, task):
"""Get sensors data.
Not implemented by this driver.
:param task: a TaskManager instance.
"""
raise NotImplementedError()

View File

@ -1218,6 +1218,76 @@ class UpdatePortTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase):
self.assertEqual(new_address, res.address)
self.assertFalse(mac_update_mock.called)
def test__filter_out_unsupported_types_all(self):
self._start_service()
CONF.set_override('send_sensor_data_types', ['All'], group='conductor')
fake_sensors_data = {"t1": {'f1': 'v1'}, "t2": {'f1': 'v1'}}
actual_result = self.service._filter_out_unsupported_types(
fake_sensors_data)
expected_result = {"t1": {'f1': 'v1'}, "t2": {'f1': 'v1'}}
self.assertEqual(expected_result, actual_result)
def test__filter_out_unsupported_types_part(self):
self._start_service()
CONF.set_override('send_sensor_data_types', ['t1'], group='conductor')
fake_sensors_data = {"t1": {'f1': 'v1'}, "t2": {'f1': 'v1'}}
actual_result = self.service._filter_out_unsupported_types(
fake_sensors_data)
expected_result = {"t1": {'f1': 'v1'}}
self.assertEqual(expected_result, actual_result)
def test__filter_out_unsupported_types_non(self):
self._start_service()
CONF.set_override('send_sensor_data_types', ['t3'], group='conductor')
fake_sensors_data = {"t1": {'f1': 'v1'}, "t2": {'f1': 'v1'}}
actual_result = self.service._filter_out_unsupported_types(
fake_sensors_data)
expected_result = {}
self.assertEqual(expected_result, actual_result)
@mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor')
@mock.patch.object(dbapi.IMPL, 'get_nodeinfo_list')
@mock.patch.object(task_manager, 'acquire')
def test___send_sensor_data(self, acquire_mock, get_nodeinfo_list_mock,
_mapped_to_this_conductor_mock):
node = obj_utils.create_test_node(self.context,
driver='fake')
self._start_service()
CONF.set_override('send_sensor_data', True, group='conductor')
acquire_mock.return_value.__enter__.return_value.driver = self.driver
with mock.patch.object(self.driver.management,
'get_sensors_data') as get_sensors_data_mock:
get_sensors_data_mock.return_value = 'fake-sensor-data'
_mapped_to_this_conductor_mock.return_value = True
get_nodeinfo_list_mock.return_value = [(node.uuid, node.driver,
node.instance_uuid)]
self.service._send_sensor_data(self.context)
self.assertTrue(get_nodeinfo_list_mock.called)
self.assertTrue(_mapped_to_this_conductor_mock.called)
self.assertTrue(acquire_mock.called)
self.assertTrue(get_sensors_data_mock.called)
@mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor')
@mock.patch.object(dbapi.IMPL, 'get_nodeinfo_list')
@mock.patch.object(task_manager, 'acquire')
def test___send_sensor_data_disabled(self, acquire_mock,
get_nodeinfo_list_mock, _mapped_to_this_conductor_mock):
node = obj_utils.create_test_node(self.context,
driver='fake')
self._start_service()
acquire_mock.return_value.__enter__.return_value.driver = self.driver
with mock.patch.object(self.driver.management,
'get_sensors_data') as get_sensors_data_mock:
get_sensors_data_mock.return_value = 'fake-sensor-data'
_mapped_to_this_conductor_mock.return_value = True
get_nodeinfo_list_mock.return_value = [(node.uuid, node.driver,
node.instance_uuid)]
self.service._send_sensor_data(self.context)
self.assertFalse(get_nodeinfo_list_mock.called)
self.assertFalse(_mapped_to_this_conductor_mock.called)
self.assertFalse(acquire_mock.called)
self.assertFalse(get_sensors_data_mock.called)
class ManagerSpawnWorkerTestCase(tests_base.TestCase):
def setUp(self):

View File

@ -917,3 +917,208 @@ class IPMIToolDriverTestCase(db_base.DbTestCase):
with task_manager.acquire(self.context, node.uuid) as task:
self.assertRaises(exception.InvalidParameterValue,
task.driver.management.validate, task)
def test__parse_ipmi_sensor_data_ok(self):
fake_sensors_data = """
Sensor ID : Temp (0x1)
Entity ID : 3.1 (Processor)
Sensor Type (Analog) : Temperature
Sensor Reading : -58 (+/- 1) degrees C
Status : ok
Nominal Reading : 50.000
Normal Minimum : 11.000
Normal Maximum : 69.000
Upper critical : 90.000
Upper non-critical : 85.000
Positive Hysteresis : 1.000
Negative Hysteresis : 1.000
Sensor ID : Temp (0x2)
Entity ID : 3.2 (Processor)
Sensor Type (Analog) : Temperature
Sensor Reading : 50 (+/- 1) degrees C
Status : ok
Nominal Reading : 50.000
Normal Minimum : 11.000
Normal Maximum : 69.000
Upper critical : 90.000
Upper non-critical : 85.000
Positive Hysteresis : 1.000
Negative Hysteresis : 1.000
Sensor ID : FAN MOD 1A RPM (0x30)
Entity ID : 7.1 (System Board)
Sensor Type (Analog) : Fan
Sensor Reading : 8400 (+/- 75) RPM
Status : ok
Nominal Reading : 5325.000
Normal Minimum : 10425.000
Normal Maximum : 14775.000
Lower critical : 4275.000
Positive Hysteresis : 375.000
Negative Hysteresis : 375.000
Sensor ID : FAN MOD 1B RPM (0x31)
Entity ID : 7.1 (System Board)
Sensor Type (Analog) : Fan
Sensor Reading : 8550 (+/- 75) RPM
Status : ok
Nominal Reading : 7800.000
Normal Minimum : 10425.000
Normal Maximum : 14775.000
Lower critical : 4275.000
Positive Hysteresis : 375.000
Negative Hysteresis : 375.000
"""
expected_return = {
'Fan': {
'FAN MOD 1A RPM (0x30)': {
'Status': 'ok',
'Sensor Reading': '8400 (+/- 75) RPM',
'Entity ID': '7.1 (System Board)',
'Normal Minimum': '10425.000',
'Positive Hysteresis': '375.000',
'Normal Maximum': '14775.000',
'Sensor Type (Analog)': 'Fan',
'Lower critical': '4275.000',
'Negative Hysteresis': '375.000',
'Sensor ID': 'FAN MOD 1A RPM (0x30)',
'Nominal Reading': '5325.000'
},
'FAN MOD 1B RPM (0x31)': {
'Status': 'ok',
'Sensor Reading': '8550 (+/- 75) RPM',
'Entity ID': '7.1 (System Board)',
'Normal Minimum': '10425.000',
'Positive Hysteresis': '375.000',
'Normal Maximum': '14775.000',
'Sensor Type (Analog)': 'Fan',
'Lower critical': '4275.000',
'Negative Hysteresis': '375.000',
'Sensor ID': 'FAN MOD 1B RPM (0x31)',
'Nominal Reading': '7800.000'
}
},
'Temperature': {
'Temp (0x1)': {
'Status': 'ok',
'Sensor Reading': '-58 (+/- 1) degrees C',
'Entity ID': '3.1 (Processor)',
'Normal Minimum': '11.000',
'Positive Hysteresis': '1.000',
'Upper non-critical': '85.000',
'Normal Maximum': '69.000',
'Sensor Type (Analog)': 'Temperature',
'Negative Hysteresis': '1.000',
'Upper critical': '90.000',
'Sensor ID': 'Temp (0x1)',
'Nominal Reading': '50.000'
},
'Temp (0x2)': {
'Status': 'ok',
'Sensor Reading': '50 (+/- 1) degrees C',
'Entity ID': '3.2 (Processor)',
'Normal Minimum': '11.000',
'Positive Hysteresis': '1.000',
'Upper non-critical': '85.000',
'Normal Maximum': '69.000',
'Sensor Type (Analog)': 'Temperature',
'Negative Hysteresis': '1.000',
'Upper critical': '90.000',
'Sensor ID': 'Temp (0x2)',
'Nominal Reading': '50.000'
}
}
}
ret = ipmi._parse_ipmi_sensors_data(self.node, fake_sensors_data)
self.assertEqual(expected_return, ret)
def test__parse_ipmi_sensor_data_missing_sensor_reading(self):
fake_sensors_data = """
Sensor ID : Temp (0x1)
Entity ID : 3.1 (Processor)
Sensor Type (Analog) : Temperature
Status : ok
Nominal Reading : 50.000
Normal Minimum : 11.000
Normal Maximum : 69.000
Upper critical : 90.000
Upper non-critical : 85.000
Positive Hysteresis : 1.000
Negative Hysteresis : 1.000
Sensor ID : Temp (0x2)
Entity ID : 3.2 (Processor)
Sensor Type (Analog) : Temperature
Sensor Reading : 50 (+/- 1) degrees C
Status : ok
Nominal Reading : 50.000
Normal Minimum : 11.000
Normal Maximum : 69.000
Upper critical : 90.000
Upper non-critical : 85.000
Positive Hysteresis : 1.000
Negative Hysteresis : 1.000
Sensor ID : FAN MOD 1A RPM (0x30)
Entity ID : 7.1 (System Board)
Sensor Type (Analog) : Fan
Sensor Reading : 8400 (+/- 75) RPM
Status : ok
Nominal Reading : 5325.000
Normal Minimum : 10425.000
Normal Maximum : 14775.000
Lower critical : 4275.000
Positive Hysteresis : 375.000
Negative Hysteresis : 375.000
"""
expected_return = {
'Fan': {
'FAN MOD 1A RPM (0x30)': {
'Status': 'ok',
'Sensor Reading': '8400 (+/- 75) RPM',
'Entity ID': '7.1 (System Board)',
'Normal Minimum': '10425.000',
'Positive Hysteresis': '375.000',
'Normal Maximum': '14775.000',
'Sensor Type (Analog)': 'Fan',
'Lower critical': '4275.000',
'Negative Hysteresis': '375.000',
'Sensor ID': 'FAN MOD 1A RPM (0x30)',
'Nominal Reading': '5325.000'
}
},
'Temperature': {
'Temp (0x2)': {
'Status': 'ok',
'Sensor Reading': '50 (+/- 1) degrees C',
'Entity ID': '3.2 (Processor)',
'Normal Minimum': '11.000',
'Positive Hysteresis': '1.000',
'Upper non-critical': '85.000',
'Normal Maximum': '69.000',
'Sensor Type (Analog)': 'Temperature',
'Negative Hysteresis': '1.000',
'Upper critical': '90.000',
'Sensor ID': 'Temp (0x2)',
'Nominal Reading': '50.000'
}
}
}
ret = ipmi._parse_ipmi_sensors_data(self.node, fake_sensors_data)
self.assertEqual(expected_return, ret)
def test__parse_ipmi_sensor_data_failed(self):
fake_sensors_data = "abcdef"
self.assertRaises(exception.FailedToParseSensorData,
ipmi._parse_ipmi_sensors_data,
self.node,
fake_sensors_data)
fake_sensors_data = "abc:def:ghi"
self.assertRaises(exception.FailedToParseSensorData,
ipmi._parse_ipmi_sensors_data,
self.node,
fake_sensors_data)