Merge "Implements send-data-to-ceilometer"
This commit is contained in:
commit
00894bc755
@ -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
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
@ -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 {}
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user