summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdwin Zhai <edwin.zhai@intel.com>2014-09-06 00:01:45 +0800
committerEdwin Zhai <edwin.zhai@intel.com>2014-09-13 15:38:25 +0800
commit1081ac1b6e32fe62c23d8b899527a7cede04b526 (patch)
treee1aa28a3f16d928b865ffe1a24af7a81d3cdc173
parenta31b1375396b203f35cc02670f87602140e5c6fa (diff)
downloadceilometer-1081ac1b6e32fe62c23d8b899527a7cede04b526.tar.gz
Add IPMI support
Adds IPMI engine to get sensor data or system power/thermal information on IPMI capable node, improves overall efficiency and maximizes overall usage for data center. Implements bp ipmi-support Change-Id: I77c881fdaf39c69cfa990468110dbbfa1f8df8dd Signed-off-by: Zhai Edwin <edwin.zhai@intel.com>
-rw-r--r--ceilometer/ipmi/__init__.py0
-rw-r--r--ceilometer/ipmi/platform/__init__.py0
-rw-r--r--ceilometer/ipmi/platform/exception.py24
-rw-r--r--ceilometer/ipmi/platform/intel_node_manager.py274
-rw-r--r--ceilometer/ipmi/platform/ipmi_sensor.py115
-rw-r--r--ceilometer/ipmi/platform/ipmitool.py132
-rw-r--r--ceilometer/openstack/common/processutils.py285
-rw-r--r--ceilometer/tests/ipmi/__init__.py0
-rw-r--r--ceilometer/tests/ipmi/platform/__init__.py0
-rw-r--r--ceilometer/tests/ipmi/platform/fake_utils.py98
-rw-r--r--ceilometer/tests/ipmi/platform/ipmitool_test_data.py359
-rw-r--r--ceilometer/tests/ipmi/platform/test_intel_node_manager.py84
-rw-r--r--ceilometer/tests/ipmi/platform/test_ipmi_sensor.py118
-rw-r--r--ceilometer/utils.py21
-rw-r--r--etc/ceilometer/rootwrap.conf27
-rw-r--r--etc/ceilometer/rootwrap.d/ipmi.filters7
-rw-r--r--openstack-common.conf1
-rw-r--r--requirements.txt2
-rw-r--r--setup.cfg1
19 files changed, 1548 insertions, 0 deletions
diff --git a/ceilometer/ipmi/__init__.py b/ceilometer/ipmi/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ceilometer/ipmi/__init__.py
diff --git a/ceilometer/ipmi/platform/__init__.py b/ceilometer/ipmi/platform/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ceilometer/ipmi/platform/__init__.py
diff --git a/ceilometer/ipmi/platform/exception.py b/ceilometer/ipmi/platform/exception.py
new file mode 100644
index 00000000..0a7d4855
--- /dev/null
+++ b/ceilometer/ipmi/platform/exception.py
@@ -0,0 +1,24 @@
+# Copyright 2014 Intel Corporation.
+# All Rights Reserved.
+#
+# 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.
+
+
+class NodeManagerException(Exception):
+ pass
+
+
+class IPMIException(Exception):
+ pass
diff --git a/ceilometer/ipmi/platform/intel_node_manager.py b/ceilometer/ipmi/platform/intel_node_manager.py
new file mode 100644
index 00000000..97ca0c16
--- /dev/null
+++ b/ceilometer/ipmi/platform/intel_node_manager.py
@@ -0,0 +1,274 @@
+# Copyright 2014 Intel Corporation.
+# All Rights Reserved.
+#
+# Author: Zhai Edwin <edwin.zhai@intel.com>
+# Author: Gao Fengqian <fengqian.gao@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.
+
+"""Node manager engine to collect power and temperature of compute node.
+
+Intel Node Manager Technology enables the datacenter IT to monitor and control
+actual server power, thermal and compute utlization behavior through industry
+defined standard IPMI. This file provides Node Manager engine to get simple
+system power and temperature data based on ipmitool.
+"""
+
+import binascii
+import tempfile
+import time
+
+from ceilometer.ipmi.platform import exception as nmexcept
+from ceilometer.ipmi.platform import ipmitool
+from ceilometer.openstack.common.gettextutils import _
+from oslo.config import cfg
+
+try:
+ import collections as ordereddict
+except ImportError:
+ import ordereddict
+
+node_manager_init_retry = cfg.IntOpt('node_manager_init_retry',
+ default=3,
+ help='Number of retries upon Intel Node '
+ 'Manager initialization failure')
+
+
+CONF = cfg.CONF
+CONF.register_opt(node_manager_init_retry, group='ipmi')
+
+IPMICMD = {"sdr_dump": "sdr dump",
+ "sdr_info": "sdr info",
+ "sensor_dump": "sdr -v"}
+IPMIRAWCMD = {"get_device_id": "raw 0x06 0x01",
+ "init_sensor_agent": "raw 0x0a 0x2c 0x01",
+ "init_complete": "raw 0x0a 0x2c 0x00",
+ "init_sensor_agent_status": "raw 0x0a 0x2c 0x00",
+ "read_power_all": "raw 0x2e 0xc8 0x57 0x01 0x00 0x01 0x00 0x00",
+ "read_temperature_all":
+ "raw 0x2e 0xc8 0x57 0x01 0x00 0x02 0x00 0x00"}
+
+MANUFACTURER_ID_INTEL = ['57', '01', '00']
+INTEL_PREFIX = '5701000d01'
+
+# The template dict are made according to the spec. It contains the expected
+# length of each item. And it can be used to parse the output of IPMI command.
+
+ONE_RETURN_TEMPLATE = {"ret": 1}
+
+BMC_INFO_TEMPLATE = ordereddict.OrderedDict()
+BMC_INFO_TEMPLATE['Device_ID'] = 1
+BMC_INFO_TEMPLATE['Device_Revision'] = 1
+BMC_INFO_TEMPLATE['Firmware_Revision_1'] = 1
+BMC_INFO_TEMPLATE['Firmware_Revision_2'] = 1
+BMC_INFO_TEMPLATE['IPMI_Version'] = 1
+BMC_INFO_TEMPLATE['Additional_Device_support'] = 1
+BMC_INFO_TEMPLATE['Manufacturer_ID'] = 3
+BMC_INFO_TEMPLATE['Product_ID'] = 2
+BMC_INFO_TEMPLATE['Auxiliary_Firmware_Revision'] = 4
+
+NM_STATISTICS_TEMPLATE = ordereddict.OrderedDict()
+NM_STATISTICS_TEMPLATE['Manufacturer_ID'] = 3
+NM_STATISTICS_TEMPLATE['Current_value'] = 2
+NM_STATISTICS_TEMPLATE['Minimum_value'] = 2
+NM_STATISTICS_TEMPLATE['Maximum_value'] = 2
+NM_STATISTICS_TEMPLATE['Average_value'] = 2
+NM_STATISTICS_TEMPLATE['Time_stamp'] = 4
+NM_STATISTICS_TEMPLATE['Report_period'] = 4
+NM_STATISTICS_TEMPLATE["DomainID_PolicyState"] = 1
+
+NM_GET_DEVICE_ID_TEMPLATE = ordereddict.OrderedDict()
+NM_GET_DEVICE_ID_TEMPLATE['Device_ID'] = 1
+NM_GET_DEVICE_ID_TEMPLATE['Device_revision'] = 1
+NM_GET_DEVICE_ID_TEMPLATE['Firmware_revision_1'] = 1
+NM_GET_DEVICE_ID_TEMPLATE['Firmware_Revision_2'] = 1
+NM_GET_DEVICE_ID_TEMPLATE['IPMI_Version'] = 1
+NM_GET_DEVICE_ID_TEMPLATE['Additinal_Device_support'] = 1
+NM_GET_DEVICE_ID_TEMPLATE['Manufacturer_ID'] = 3
+NM_GET_DEVICE_ID_TEMPLATE['Product_ID_min_version'] = 1
+NM_GET_DEVICE_ID_TEMPLATE['Product_ID_major_version'] = 1
+NM_GET_DEVICE_ID_TEMPLATE['Implemented_firmware'] = 1
+NM_GET_DEVICE_ID_TEMPLATE['Firmware_build_number'] = 1
+NM_GET_DEVICE_ID_TEMPLATE['Last_digit_firmware_build_number'] = 1
+NM_GET_DEVICE_ID_TEMPLATE['Image_flags'] = 1
+
+
+def _hex(list=[]):
+ """Format the return value in list into hex."""
+ if list:
+ list.reverse()
+ return int(''.join(list), 16)
+
+ return 0
+
+
+class NodeManager(object):
+ """The python implementation of Intel Node Manager engine using ipmitool
+
+ The class implements the engine to read power and temperature of
+ compute node. It uses ipmitool to execute the IPMI command and parse
+ the output into dict.
+ """
+ _inited = False
+ _instance = None
+
+ def __new__(cls, *args, **kwargs):
+ """Singleton to avoid duplicated initialization."""
+ if not cls._instance:
+ cls._instance = super(NodeManager, cls).__new__(cls, *args,
+ **kwargs)
+ return cls._instance
+
+ def __init__(self):
+ if not (self._instance and self._inited):
+ self.nm_support = False
+ self.channel_slave = ''
+ self._inited = True
+
+ self.nm_support = self.check_node_manager()
+
+ @staticmethod
+ def _parse_slave_and_channel(file_path):
+ """Parse the dumped file to get slave address and channel number.
+
+ :param file_path: file path of dumped SDR file.
+ :return: slave address and channel number of target device.
+ """
+ ret = None
+ prefix = INTEL_PREFIX
+ # According to Intel Node Manager spec, section 4.5, for Intel NM
+ # discovery OEM SDR records are type C0h. It contains manufacture ID
+ # and OEM data in the record body.
+ # 0-2 bytes are OEM ID, byte 3 is 0Dh and byte 4 is 01h. Byte 5, 6
+ # is Intel NM device slave address and channel number/sensor owner LUN.
+ with open(file_path, 'rb') as bin_fp:
+ for line in bin_fp.readlines():
+ if line:
+ data_str = binascii.hexlify(line)
+ if prefix in data_str:
+ oem_id_index = data_str.index(prefix)
+ ret = data_str[oem_id_index + len(prefix):
+ oem_id_index + len(prefix) + 4]
+ # Byte 5 is slave address. [7:4] from byte 6 is channel
+ # number, so just pick ret[2] here.
+ ret = (ret[0:2], ret[2])
+ break
+ return ret
+
+ @ipmitool.execute_ipmi_cmd(BMC_INFO_TEMPLATE)
+ def get_device_id(self):
+ """IPMI command GET_DEVICE_ID."""
+ return IPMIRAWCMD["get_device_id"]
+
+ @ipmitool.execute_ipmi_cmd(ONE_RETURN_TEMPLATE)
+ def _init_sensor_agent(self):
+ """Run initialization agent."""
+ return IPMIRAWCMD["init_sensor_agent"]
+
+ @ipmitool.execute_ipmi_cmd(ONE_RETURN_TEMPLATE)
+ def _init_sensor_agent_process(self):
+ """Check the status of initialization agent."""
+ return IPMIRAWCMD["init_sensor_agent_status"]
+
+ @ipmitool.execute_ipmi_cmd()
+ def _dump_sdr_file(self, data_file=""):
+ """Dump SDR into a file."""
+ return IPMICMD["sdr_dump"] + " " + data_file
+
+ @ipmitool.execute_ipmi_cmd(NM_GET_DEVICE_ID_TEMPLATE)
+ def _node_manager_get_device_id(self):
+ """GET_DEVICE_ID command in Intel Node Manager
+
+ Different from IPMI command GET_DEVICE_ID, it contains more information
+ of Intel Node Manager.
+ """
+ return self.channel_slave + ' ' + IPMIRAWCMD["get_device_id"]
+
+ @ipmitool.execute_ipmi_cmd(NM_STATISTICS_TEMPLATE)
+ def _read_power_all(self):
+ """Get the power consumption of the whole platform."""
+ return self.channel_slave + ' ' + IPMIRAWCMD['read_power_all']
+
+ @ipmitool.execute_ipmi_cmd(NM_STATISTICS_TEMPLATE)
+ def _read_temperature_all(self):
+ """Get the temperature info of the whole platform."""
+ return self.channel_slave + ' ' + IPMIRAWCMD['read_temperature_all']
+
+ def read_power_all(self):
+ if self.nm_support:
+ return self._read_power_all()
+
+ return {}
+
+ def read_temperature_all(self):
+ if self.nm_support:
+ return self._read_temperature_all()
+
+ return {}
+
+ def init_node_manager(self):
+ if self._init_sensor_agent_process()['ret'] == ['01']:
+ return
+ # Run sensor initialization agent
+ for i in range(CONF.ipmi.node_manager_init_retry):
+ self._init_sensor_agent()
+ time.sleep(1)
+ if self._init_sensor_agent_process()['ret'] == ['01']:
+ return
+
+ raise nmexcept.NodeManagerException(_('Node Manager init failed'))
+
+ def discover_slave_channel(self):
+ """Discover target slave address and channel number."""
+ file_path = tempfile.mkstemp()[1]
+ self._dump_sdr_file(data_file=file_path)
+ ret = self._parse_slave_and_channel(file_path)
+ slave_address = ''.join(['0x', ret[0]])
+ channel = ''.join(['0x', ret[1]])
+ # String of channel and slave_address
+ self.channel_slave = '-b ' + channel + ' -t ' + slave_address
+
+ def node_manager_support(self):
+ """Intel Node Manager capability checking
+
+ This function is used to detect if compute node support Intel
+ Node Manager or not and parse out the slave address and channel
+ number of node manager.
+ """
+ self.manufacturer_id = self.get_device_id()['Manufacturer_ID']
+ if MANUFACTURER_ID_INTEL != self.manufacturer_id:
+ # If the manufacturer is not Intel, just set False and return.
+ return False
+
+ self.discover_slave_channel()
+ support = self._node_manager_get_device_id()['Implemented_firmware']
+ # According to Intel Node Manager spec, return value of GET_DEVICE_ID,
+ # bits 3 to 0 shows if Intel NM implemented or not.
+ if int(support[0], 16) & 0xf != 0:
+ return True
+ else:
+ return False
+
+ def check_node_manager(self):
+ """Intel Node Manager init and check
+
+ This function is used to initialize Intel Node Manager and check the
+ capability without throwing exception. It's safe to call it on
+ non-NodeManager platform.
+ """
+ try:
+ self.init_node_manager()
+ has_nm = self.node_manager_support()
+ except (nmexcept.NodeManagerException, nmexcept.IPMIException):
+ return False
+ return has_nm
diff --git a/ceilometer/ipmi/platform/ipmi_sensor.py b/ceilometer/ipmi/platform/ipmi_sensor.py
new file mode 100644
index 00000000..9749c485
--- /dev/null
+++ b/ceilometer/ipmi/platform/ipmi_sensor.py
@@ -0,0 +1,115 @@
+# Copyright 2014 Intel Corporation.
+#
+# 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.
+
+"""IPMI sensor to collect various sensor data of compute node"""
+
+from ceilometer.ipmi.platform import exception as ipmiexcept
+from ceilometer.ipmi.platform import ipmitool
+from ceilometer.openstack.common.gettextutils import _
+
+IPMICMD = {"sdr_dump": "sdr dump",
+ "sdr_info": "sdr info",
+ "sensor_dump": "sdr -v",
+ "sensor_dump_temperature": "sdr -v type Temperature",
+ "sensor_dump_current": "sdr -v type Current",
+ "sensor_dump_fan": "sdr -v type Fan",
+ "sensor_dump_voltage": "sdr -v type Voltage"}
+
+# Requires translation of output into dict
+DICT_TRANSLATE_TEMPLATE = {"translate": 1}
+
+
+class IPMISensor(object):
+ """The python implementation of IPMI sensor using ipmitool
+
+ The class implements the IPMI sensor to get various sensor data of
+ compute node. It uses ipmitool to execute the IPMI command and parse
+ the output into dict.
+ """
+ _inited = False
+ _instance = None
+
+ def __new__(cls, *args, **kwargs):
+ """Singleton to avoid duplicated initialization."""
+ if not cls._instance:
+ cls._instance = super(IPMISensor, cls).__new__(cls, *args,
+ **kwargs)
+ return cls._instance
+
+ def __init__(self):
+ if not (self._instance and self._inited):
+ self.ipmi_support = False
+ self._inited = True
+
+ self.ipmi_support = self.check_ipmi()
+
+ @ipmitool.execute_ipmi_cmd()
+ def _get_sdr_info(self):
+ """Get the SDR info."""
+ return IPMICMD['sdr_info']
+
+ @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE)
+ def _read_sensor_all(self):
+ """Get the sensor data for type."""
+ return IPMICMD['sensor_dump']
+
+ @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE)
+ def _read_sensor_temperature(self):
+ """Get the sensor data for Temperature."""
+ return IPMICMD['sensor_dump_temperature']
+
+ @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE)
+ def _read_sensor_voltage(self):
+ """Get the sensor data for Voltage."""
+ return IPMICMD['sensor_dump_voltage']
+
+ @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE)
+ def _read_sensor_current(self):
+ """Get the sensor data for Current."""
+ return IPMICMD['sensor_dump_current']
+
+ @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE)
+ def _read_sensor_fan(self):
+ """Get the sensor data for Fan."""
+ return IPMICMD['sensor_dump_fan']
+
+ def read_sensor_any(self, sensor_type=''):
+ """Get the sensor data for type."""
+ if not self.ipmi_support:
+ return {}
+
+ mapping = {'': self._read_sensor_all,
+ 'Temperature': self._read_sensor_temperature,
+ 'Fan': self._read_sensor_fan,
+ 'Voltage': self._read_sensor_voltage,
+ 'Current': self._read_sensor_current}
+
+ try:
+ return mapping[sensor_type]()
+ except KeyError:
+ raise ipmiexcept.IPMIException(_('Wrong sensor type'))
+
+ def check_ipmi(self):
+ """IPMI capability checking
+
+ This function is used to detect if compute node is IPMI capable
+ platform. Just run a simple IPMI command to get SDR info for check.
+ """
+ try:
+ self._get_sdr_info()
+ except ipmiexcept.IPMIException:
+ return False
+ return True
diff --git a/ceilometer/ipmi/platform/ipmitool.py b/ceilometer/ipmi/platform/ipmitool.py
new file mode 100644
index 00000000..a222b5f0
--- /dev/null
+++ b/ceilometer/ipmi/platform/ipmitool.py
@@ -0,0 +1,132 @@
+# Copyright 2014 Intel Corp.
+#
+# Author: Zhai Edwin <edwin.zhai@intel.com>
+# Author: whaom <whaom@cn.ibm.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.
+
+"""Utils to run ipmitool for data collection"""
+
+from ceilometer.ipmi.platform import exception as ipmiexcept
+from ceilometer.openstack.common.gettextutils import _
+from ceilometer.openstack.common import processutils
+from ceilometer import utils
+
+
+# Following 2 functions are copied from ironic project to handle ipmitool's
+# sensor data output. Need code clean and sharing in future.
+# Check ironic/drivers/modules/ipmitool.py
+
+
+def _get_sensor_type(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 ipmiexcept.IPMIException(_("parse IPMI sensor data failed,"
+ "unknown sensor type"))
+
+
+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 _translate_output(output):
+ """Translate the return value into JSON dict
+
+ :param output: output of the execution of IPMI command(sensor reading)
+ """
+ sensors_data_dict = {}
+
+ sensors_data_array = output.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(sensor_data_dict)
+
+ # ignore the sensors which have no current 'Sensor Reading' data
+ sensor_id = sensor_data_dict['Sensor ID']
+ if 'Sensor Reading' in sensor_data_dict:
+ sensors_data_dict.setdefault(sensor_type,
+ {})[sensor_id] = sensor_data_dict
+
+ # get nothing, no valid sensor data
+ if not sensors_data_dict:
+ raise ipmiexcept.IPMIException(_("parse IPMI sensor data failed,"
+ "No data retrieved from given input"))
+ return sensors_data_dict
+
+
+def _parse_output(output, template):
+ """Parse the return value of IPMI command into dict
+
+ :param output: output of the execution of IPMI command
+ :param template: a dict that contains the expected items of
+ IPMI command and its length.
+ """
+ ret = {}
+ index = 0
+ if not (output and template):
+ return ret
+
+ if "translate" in template:
+ ret = _translate_output(output)
+ else:
+ output_list = output.strip().split(' ')
+ if sum(template.values()) != len(output_list):
+ raise ipmiexcept.IPMIException(_("ipmitool output "
+ "length mismatch"))
+ for item in template.items():
+ index_end = index + item[1]
+ update_value = output_list[index: index_end]
+ ret[item[0]] = update_value
+ index = index_end
+ return ret
+
+
+def execute_ipmi_cmd(template={}):
+ """Decorator for the execution of IPMI command.
+
+ It parses the output of IPMI command into dictionary.
+ """
+ def _execute_ipmi_cmd(f):
+ def _execute(self, **kwargs):
+ args = ['ipmitool']
+ command = f(self, **kwargs)
+ args.extend(command.split(" "))
+ try:
+ (out, __) = utils.execute(*args, run_as_root=True)
+ except processutils.ProcessExecutionError:
+ raise ipmiexcept.IPMIException(_("running ipmitool failure"))
+ return _parse_output(out, template)
+ return _execute
+
+ return _execute_ipmi_cmd
diff --git a/ceilometer/openstack/common/processutils.py b/ceilometer/openstack/common/processutils.py
new file mode 100644
index 00000000..ebb3cb74
--- /dev/null
+++ b/ceilometer/openstack/common/processutils.py
@@ -0,0 +1,285 @@
+# Copyright 2011 OpenStack Foundation.
+# All Rights Reserved.
+#
+# 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.
+
+"""
+System-level utilities and helper functions.
+"""
+
+import errno
+import logging
+import multiprocessing
+import os
+import random
+import shlex
+import signal
+
+from eventlet.green import subprocess
+from eventlet import greenthread
+import six
+
+from ceilometer.openstack.common.gettextutils import _
+from ceilometer.openstack.common import strutils
+
+
+LOG = logging.getLogger(__name__)
+
+
+class InvalidArgumentError(Exception):
+ def __init__(self, message=None):
+ super(InvalidArgumentError, self).__init__(message)
+
+
+class UnknownArgumentError(Exception):
+ def __init__(self, message=None):
+ super(UnknownArgumentError, self).__init__(message)
+
+
+class ProcessExecutionError(Exception):
+ def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
+ description=None):
+ self.exit_code = exit_code
+ self.stderr = stderr
+ self.stdout = stdout
+ self.cmd = cmd
+ self.description = description
+
+ if description is None:
+ description = _("Unexpected error while running command.")
+ if exit_code is None:
+ exit_code = '-'
+ message = _('%(description)s\n'
+ 'Command: %(cmd)s\n'
+ 'Exit code: %(exit_code)s\n'
+ 'Stdout: %(stdout)r\n'
+ 'Stderr: %(stderr)r') % {'description': description,
+ 'cmd': cmd,
+ 'exit_code': exit_code,
+ 'stdout': stdout,
+ 'stderr': stderr}
+ super(ProcessExecutionError, self).__init__(message)
+
+
+class NoRootWrapSpecified(Exception):
+ def __init__(self, message=None):
+ super(NoRootWrapSpecified, self).__init__(message)
+
+
+def _subprocess_setup():
+ # Python installs a SIGPIPE handler by default. This is usually not what
+ # non-Python subprocesses expect.
+ signal.signal(signal.SIGPIPE, signal.SIG_DFL)
+
+
+def execute(*cmd, **kwargs):
+ """Helper method to shell out and execute a command through subprocess.
+
+ Allows optional retry.
+
+ :param cmd: Passed to subprocess.Popen.
+ :type cmd: string
+ :param process_input: Send to opened process.
+ :type process_input: string
+ :param env_variables: Environment variables and their values that
+ will be set for the process.
+ :type env_variables: dict
+ :param check_exit_code: Single bool, int, or list of allowed exit
+ codes. Defaults to [0]. Raise
+ :class:`ProcessExecutionError` unless
+ program exits with one of these code.
+ :type check_exit_code: boolean, int, or [int]
+ :param delay_on_retry: True | False. Defaults to True. If set to True,
+ wait a short amount of time before retrying.
+ :type delay_on_retry: boolean
+ :param attempts: How many times to retry cmd.
+ :type attempts: int
+ :param run_as_root: True | False. Defaults to False. If set to True,
+ the command is prefixed by the command specified
+ in the root_helper kwarg.
+ :type run_as_root: boolean
+ :param root_helper: command to prefix to commands called with
+ run_as_root=True
+ :type root_helper: string
+ :param shell: whether or not there should be a shell used to
+ execute this command. Defaults to false.
+ :type shell: boolean
+ :param loglevel: log level for execute commands.
+ :type loglevel: int. (Should be logging.DEBUG or logging.INFO)
+ :returns: (stdout, stderr) from process execution
+ :raises: :class:`UnknownArgumentError` on
+ receiving unknown arguments
+ :raises: :class:`ProcessExecutionError`
+ """
+
+ process_input = kwargs.pop('process_input', None)
+ env_variables = kwargs.pop('env_variables', None)
+ check_exit_code = kwargs.pop('check_exit_code', [0])
+ ignore_exit_code = False
+ delay_on_retry = kwargs.pop('delay_on_retry', True)
+ attempts = kwargs.pop('attempts', 1)
+ run_as_root = kwargs.pop('run_as_root', False)
+ root_helper = kwargs.pop('root_helper', '')
+ shell = kwargs.pop('shell', False)
+ loglevel = kwargs.pop('loglevel', logging.DEBUG)
+
+ if isinstance(check_exit_code, bool):
+ ignore_exit_code = not check_exit_code
+ check_exit_code = [0]
+ elif isinstance(check_exit_code, int):
+ check_exit_code = [check_exit_code]
+
+ if kwargs:
+ raise UnknownArgumentError(_('Got unknown keyword args: %r') % kwargs)
+
+ if run_as_root and hasattr(os, 'geteuid') and os.geteuid() != 0:
+ if not root_helper:
+ raise NoRootWrapSpecified(
+ message=_('Command requested root, but did not '
+ 'specify a root helper.'))
+ cmd = shlex.split(root_helper) + list(cmd)
+
+ cmd = map(str, cmd)
+ sanitized_cmd = strutils.mask_password(' '.join(cmd))
+
+ while attempts > 0:
+ attempts -= 1
+ try:
+ LOG.log(loglevel, _('Running cmd (subprocess): %s'), sanitized_cmd)
+ _PIPE = subprocess.PIPE # pylint: disable=E1101
+
+ if os.name == 'nt':
+ preexec_fn = None
+ close_fds = False
+ else:
+ preexec_fn = _subprocess_setup
+ close_fds = True
+
+ obj = subprocess.Popen(cmd,
+ stdin=_PIPE,
+ stdout=_PIPE,
+ stderr=_PIPE,
+ close_fds=close_fds,
+ preexec_fn=preexec_fn,
+ shell=shell,
+ env=env_variables)
+ result = None
+ for _i in six.moves.range(20):
+ # NOTE(russellb) 20 is an arbitrary number of retries to
+ # prevent any chance of looping forever here.
+ try:
+ if process_input is not None:
+ result = obj.communicate(process_input)
+ else:
+ result = obj.communicate()
+ except OSError as e:
+ if e.errno in (errno.EAGAIN, errno.EINTR):
+ continue
+ raise
+ break
+ obj.stdin.close() # pylint: disable=E1101
+ _returncode = obj.returncode # pylint: disable=E1101
+ LOG.log(loglevel, 'Result was %s' % _returncode)
+ if not ignore_exit_code and _returncode not in check_exit_code:
+ (stdout, stderr) = result
+ sanitized_stdout = strutils.mask_password(stdout)
+ sanitized_stderr = strutils.mask_password(stderr)
+ raise ProcessExecutionError(exit_code=_returncode,
+ stdout=sanitized_stdout,
+ stderr=sanitized_stderr,
+ cmd=sanitized_cmd)
+ return result
+ except ProcessExecutionError:
+ if not attempts:
+ raise
+ else:
+ LOG.log(loglevel, _('%r failed. Retrying.'), sanitized_cmd)
+ if delay_on_retry:
+ greenthread.sleep(random.randint(20, 200) / 100.0)
+ finally:
+ # NOTE(termie): this appears to be necessary to let the subprocess
+ # call clean something up in between calls, without
+ # it two execute calls in a row hangs the second one
+ greenthread.sleep(0)
+
+
+def trycmd(*args, **kwargs):
+ """A wrapper around execute() to more easily handle warnings and errors.
+
+ Returns an (out, err) tuple of strings containing the output of
+ the command's stdout and stderr. If 'err' is not empty then the
+ command can be considered to have failed.
+
+ :discard_warnings True | False. Defaults to False. If set to True,
+ then for succeeding commands, stderr is cleared
+
+ """
+ discard_warnings = kwargs.pop('discard_warnings', False)
+
+ try:
+ out, err = execute(*args, **kwargs)
+ failed = False
+ except ProcessExecutionError as exn:
+ out, err = '', six.text_type(exn)
+ failed = True
+
+ if not failed and discard_warnings and err:
+ # Handle commands that output to stderr but otherwise succeed
+ err = ''
+
+ return out, err
+
+
+def ssh_execute(ssh, cmd, process_input=None,
+ addl_env=None, check_exit_code=True):
+ LOG.debug('Running cmd (SSH): %s', cmd)
+ if addl_env:
+ raise InvalidArgumentError(_('Environment not supported over SSH'))
+
+ if process_input:
+ # This is (probably) fixable if we need it...
+ raise InvalidArgumentError(_('process_input not supported over SSH'))
+
+ stdin_stream, stdout_stream, stderr_stream = ssh.exec_command(cmd)
+ channel = stdout_stream.channel
+
+ # NOTE(justinsb): This seems suspicious...
+ # ...other SSH clients have buffering issues with this approach
+ stdout = stdout_stream.read()
+ stderr = stderr_stream.read()
+ stdin_stream.close()
+
+ exit_status = channel.recv_exit_status()
+
+ # exit_status == -1 if no exit code was returned
+ if exit_status != -1:
+ LOG.debug('Result was %s' % exit_status)
+ if check_exit_code and exit_status != 0:
+ raise ProcessExecutionError(exit_code=exit_status,
+ stdout=stdout,
+ stderr=stderr,
+ cmd=cmd)
+
+ return (stdout, stderr)
+
+
+def get_worker_count():
+ """Utility to get the default worker count.
+
+ @return: The number of CPUs if that can be determined, else a default
+ worker count of 1 is returned.
+ """
+ try:
+ return multiprocessing.cpu_count()
+ except NotImplementedError:
+ return 1
diff --git a/ceilometer/tests/ipmi/__init__.py b/ceilometer/tests/ipmi/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ceilometer/tests/ipmi/__init__.py
diff --git a/ceilometer/tests/ipmi/platform/__init__.py b/ceilometer/tests/ipmi/platform/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ceilometer/tests/ipmi/platform/__init__.py
diff --git a/ceilometer/tests/ipmi/platform/fake_utils.py b/ceilometer/tests/ipmi/platform/fake_utils.py
new file mode 100644
index 00000000..d5fe47fb
--- /dev/null
+++ b/ceilometer/tests/ipmi/platform/fake_utils.py
@@ -0,0 +1,98 @@
+# Copyright 2014 Intel Corp.
+#
+# 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 binascii
+
+from ceilometer.ipmi.platform import exception as nmexcept
+from ceilometer.ipmi.platform import intel_node_manager as node_manager
+from ceilometer.tests.ipmi.platform import ipmitool_test_data as test_data
+
+
+def get_sensor_status_init(parameter=''):
+ return (' 01\n', '')
+
+
+def get_sensor_status_uninit(parameter=''):
+ return (' 00\n', '')
+
+
+def init_sensor_agent(parameter=''):
+ return (' 00\n', '')
+
+
+def sdr_dump(data_file=''):
+ if data_file == '':
+ raise ValueError("No file specified for ipmitool sdr dump")
+ fake_slave_address = '2c'
+ fake_channel = '60'
+ hexstr = node_manager.INTEL_PREFIX + fake_slave_address + fake_channel
+ data = binascii.unhexlify(hexstr)
+ with open(data_file, 'wb') as bin_fp:
+ bin_fp.write(data)
+
+ return ('', '')
+
+
+def _execute(funcs, *cmd, **kwargs):
+
+ datas = {
+ test_data.device_id_cmd: test_data.device_id,
+ test_data.nm_device_id_cmd: test_data.nm_device_id,
+ test_data.get_power_cmd: test_data.power_data,
+ test_data.get_temperature_cmd: test_data.temperature_data,
+ test_data.sdr_info_cmd: test_data.sdr_info,
+ test_data.read_sensor_temperature_cmd: test_data.sensor_temperature,
+ test_data.read_sensor_voltage_cmd: test_data.sensor_voltage,
+ test_data.read_sensor_current_cmd: test_data.sensor_current,
+ test_data.read_sensor_fan_cmd: test_data.sensor_fan,
+ }
+
+ if cmd[1] == 'sdr' and cmd[2] == 'dump':
+ # ipmitool sdr dump /tmp/XXXX
+ cmd_str = "".join(cmd[:3])
+ par_str = cmd[3]
+ else:
+ cmd_str = "".join(cmd)
+ par_str = ''
+
+ try:
+ return datas[cmd_str]
+ except KeyError:
+ return funcs[cmd_str](par_str)
+
+
+def execute_with_nm(*cmd, **kwargs):
+ """test version of execute on Node Manager platform."""
+
+ funcs = {test_data.sensor_status_cmd: get_sensor_status_init,
+ test_data.init_sensor_cmd: init_sensor_agent,
+ test_data.sdr_dump_cmd: sdr_dump}
+
+ return _execute(funcs, *cmd, **kwargs)
+
+
+def execute_without_nm(*cmd, **kwargs):
+ """test version of execute on Non-Node Manager platform."""
+
+ funcs = {test_data.sensor_status_cmd: get_sensor_status_uninit,
+ test_data.init_sensor_cmd: init_sensor_agent,
+ test_data.sdr_dump_cmd: sdr_dump}
+
+ return _execute(funcs, *cmd, **kwargs)
+
+
+def execute_without_ipmi(*cmd, **kwargs):
+ raise nmexcept.IPMIException
diff --git a/ceilometer/tests/ipmi/platform/ipmitool_test_data.py b/ceilometer/tests/ipmi/platform/ipmitool_test_data.py
new file mode 100644
index 00000000..b64580e9
--- /dev/null
+++ b/ceilometer/tests/ipmi/platform/ipmitool_test_data.py
@@ -0,0 +1,359 @@
+# Copyright 2014 Intel Corp.
+#
+# 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.
+
+"""Sample data for test_intel_node_manager and test_ipmi_sensor.
+
+This data is provided as a sample of the data expected from the ipmitool
+binary, which produce Node Manager/IPMI raw data
+"""
+
+sensor_temperature_data = """Sensor ID : SSB Therm Trip (0xd)
+ Entity ID : 7.1 (System Board)
+ Sensor Type (Discrete): Temperature
+ Assertions Enabled : Digital State
+ [State Asserted]
+ Deassertions Enabled : Digital State
+ [State Asserted]
+
+Sensor ID : BB P1 VR Temp (0x20)
+ Entity ID : 7.1 (System Board)
+ Sensor Type (Analog) : Temperature
+ Sensor Reading : 25 (+/- 0) degrees C
+ Status : ok
+ Nominal Reading : 58.000
+ Normal Minimum : 10.000
+ Normal Maximum : 105.000
+ Upper critical : 115.000
+ Upper non-critical : 110.000
+ Lower critical : 0.000
+ Lower non-critical : 5.000
+ Positive Hysteresis : 2.000
+ Negative Hysteresis : 2.000
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : lcr lnc unc ucr
+ Settable Thresholds : lcr lnc unc ucr
+ Threshold Read Mask : lcr lnc unc ucr
+ Assertion Events :
+ Assertions Enabled : lnc- lcr- unc+ ucr+
+ Deassertions Enabled : lnc- lcr- unc+ ucr+
+
+Sensor ID : Front Panel Temp (0x21)
+ Entity ID : 12.1 (Front Panel Board)
+ Sensor Type (Analog) : Temperature
+ Sensor Reading : 23 (+/- 0) degrees C
+ Status : ok
+ Nominal Reading : 28.000
+ Normal Minimum : 10.000
+ Normal Maximum : 45.000
+ Upper critical : 55.000
+ Upper non-critical : 50.000
+ Lower critical : 0.000
+ Lower non-critical : 5.000
+ Positive Hysteresis : 2.000
+ Negative Hysteresis : 2.000
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : lcr lnc unc ucr
+ Settable Thresholds : lcr lnc unc ucr
+ Threshold Read Mask : lcr lnc unc ucr
+ Assertion Events :
+ Assertions Enabled : lnc- lcr- unc+ ucr+
+ Deassertions Enabled : lnc- lcr- unc+ ucr+
+
+Sensor ID : SSB Temp (0x22)
+ Entity ID : 7.1 (System Board)
+ Sensor Type (Analog) : Temperature
+ Sensor Reading : 43 (+/- 0) degrees C
+ Status : ok
+ Nominal Reading : 52.000
+ Normal Minimum : 10.000
+ Normal Maximum : 93.000
+ Upper critical : 103.000
+ Upper non-critical : 98.000
+ Lower critical : 0.000
+ Lower non-critical : 5.000
+ Positive Hysteresis : 2.000
+ Negative Hysteresis : 2.000
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : lcr lnc unc ucr
+ Settable Thresholds : lcr lnc unc ucr
+ Threshold Read Mask : lcr lnc unc ucr
+ Assertion Events :
+ Assertions Enabled : lnc- lcr- unc+ ucr+
+ Deassertions Enabled : lnc- lcr- unc+ ucr+
+
+"""
+
+sensor_voltage_data = """Sensor ID : VR Watchdog (0xb)
+ Entity ID : 7.1 (System Board)
+ Sensor Type (Discrete): Voltage
+ Assertions Enabled : Digital State
+ [State Asserted]
+ Deassertions Enabled : Digital State
+ [State Asserted]
+
+Sensor ID : BB +12.0V (0xd0)
+ Entity ID : 7.1 (System Board)
+ Sensor Type (Analog) : Voltage
+ Sensor Reading : 11.831 (+/- 0) Volts
+ Status : ok
+ Nominal Reading : 11.935
+ Normal Minimum : 11.363
+ Normal Maximum : 12.559
+ Upper critical : 13.391
+ Upper non-critical : 13.027
+ Lower critical : 10.635
+ Lower non-critical : 10.947
+ Positive Hysteresis : 0.052
+ Negative Hysteresis : 0.052
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : lcr lnc unc ucr
+ Settable Thresholds : lcr lnc unc ucr
+ Threshold Read Mask : lcr lnc unc ucr
+ Assertion Events :
+ Assertions Enabled : lnc- lcr- unc+ ucr+
+ Deassertions Enabled : lnc- lcr- unc+ ucr+
+
+Sensor ID : BB +1.35 P1LV AB (0xe4)
+ Entity ID : 7.1 (System Board)
+ Sensor Type (Analog) : Voltage
+ Sensor Reading : Disabled
+ Status : Disabled
+ Nominal Reading : 1.342
+ Normal Minimum : 1.275
+ Normal Maximum : 1.409
+ Upper critical : 1.488
+ Upper non-critical : 1.445
+ Lower critical : 1.201
+ Lower non-critical : 1.244
+ Positive Hysteresis : 0.006
+ Negative Hysteresis : 0.006
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : lcr lnc unc ucr
+ Settable Thresholds : lcr lnc unc ucr
+ Threshold Read Mask : lcr lnc unc ucr
+ Event Status : Unavailable
+ Assertions Enabled : lnc- lcr- unc+ ucr+
+ Deassertions Enabled : lnc- lcr- unc+ ucr+
+
+Sensor ID : BB +5.0V (0xd1)
+ Entity ID : 7.1 (System Board)
+ Sensor Type (Analog) : Voltage
+ Sensor Reading : 4.959 (+/- 0) Volts
+ Status : ok
+ Nominal Reading : 4.981
+ Normal Minimum : 4.742
+ Normal Maximum : 5.241
+ Upper critical : 5.566
+ Upper non-critical : 5.415
+ Lower critical : 4.416
+ Lower non-critical : 4.546
+ Positive Hysteresis : 0.022
+ Negative Hysteresis : 0.022
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : lcr lnc unc ucr
+ Settable Thresholds : lcr lnc unc ucr
+ Threshold Read Mask : lcr lnc unc ucr
+ Assertion Events :
+ Assertions Enabled : lnc- lcr- unc+ ucr+
+ Deassertions Enabled : lnc- lcr- unc+ ucr+
+
+"""
+
+sensor_current_data = """Sensor ID : PS1 Curr Out % (0x58)
+ Entity ID : 10.1 (Power Supply)
+ Sensor Type (Analog) : Current
+ Sensor Reading : 11 (+/- 0) unspecified
+ Status : ok
+ Nominal Reading : 50.000
+ Normal Minimum : 0.000
+ Normal Maximum : 100.000
+ Upper critical : 118.000
+ Upper non-critical : 100.000
+ Positive Hysteresis : Unspecified
+ Negative Hysteresis : Unspecified
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : unc ucr
+ Settable Thresholds : unc ucr
+ Threshold Read Mask : unc ucr
+ Assertion Events :
+ Assertions Enabled : unc+ ucr+
+ Deassertions Enabled : unc+ ucr+
+
+Sensor ID : PS2 Curr Out % (0x59)
+ Entity ID : 10.2 (Power Supply)
+ Sensor Type (Analog) : Current
+ Sensor Reading : 0 (+/- 0) unspecified
+ Status : ok
+ Nominal Reading : 50.000
+ Normal Minimum : 0.000
+ Normal Maximum : 100.000
+ Upper critical : 118.000
+ Upper non-critical : 100.000
+ Positive Hysteresis : Unspecified
+ Negative Hysteresis : Unspecified
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : unc ucr
+ Settable Thresholds : unc ucr
+ Threshold Read Mask : unc ucr
+ Assertion Events :
+ Assertions Enabled : unc+ ucr+
+ Deassertions Enabled : unc+ ucr+
+
+"""
+
+sensor_fan_data = """Sensor ID : System Fan 1 (0x30)
+ Entity ID : 29.1 (Fan Device)
+ Sensor Type (Analog) : Fan
+ Sensor Reading : 4704 (+/- 0) RPM
+ Status : ok
+ Nominal Reading : 7497.000
+ Normal Minimum : 2499.000
+ Normal Maximum : 12495.000
+ Lower critical : 1715.000
+ Lower non-critical : 1960.000
+ Positive Hysteresis : 49.000
+ Negative Hysteresis : 49.000
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : lcr lnc
+ Settable Thresholds : lcr lnc
+ Threshold Read Mask : lcr lnc
+ Assertion Events :
+ Assertions Enabled : lnc- lcr-
+ Deassertions Enabled : lnc- lcr-
+
+Sensor ID : System Fan 2 (0x32)
+ Entity ID : 29.2 (Fan Device)
+ Sensor Type (Analog) : Fan
+ Sensor Reading : 4704 (+/- 0) RPM
+ Status : ok
+ Nominal Reading : 7497.000
+ Normal Minimum : 2499.000
+ Normal Maximum : 12495.000
+ Lower critical : 1715.000
+ Lower non-critical : 1960.000
+ Positive Hysteresis : 49.000
+ Negative Hysteresis : 49.000
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : lcr lnc
+ Settable Thresholds : lcr lnc
+ Threshold Read Mask : lcr lnc
+ Assertion Events :
+ Assertions Enabled : lnc- lcr-
+ Deassertions Enabled : lnc- lcr-
+
+Sensor ID : System Fan 3 (0x34)
+ Entity ID : 29.3 (Fan Device)
+ Sensor Type (Analog) : Fan
+ Sensor Reading : 4704 (+/- 0) RPM
+ Status : ok
+ Nominal Reading : 7497.000
+ Normal Minimum : 2499.000
+ Normal Maximum : 12495.000
+ Lower critical : 1715.000
+ Lower non-critical : 1960.000
+ Positive Hysteresis : 49.000
+ Negative Hysteresis : 49.000
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : lcr lnc
+ Settable Thresholds : lcr lnc
+ Threshold Read Mask : lcr lnc
+ Assertion Events :
+ Assertions Enabled : lnc- lcr-
+ Deassertions Enabled : lnc- lcr-
+
+Sensor ID : System Fan 4 (0x36)
+ Entity ID : 29.4 (Fan Device)
+ Sensor Type (Analog) : Fan
+ Sensor Reading : 4606 (+/- 0) RPM
+ Status : ok
+ Nominal Reading : 7497.000
+ Normal Minimum : 2499.000
+ Normal Maximum : 12495.000
+ Lower critical : 1715.000
+ Lower non-critical : 1960.000
+ Positive Hysteresis : 49.000
+ Negative Hysteresis : 49.000
+ Minimum sensor range : Unspecified
+ Maximum sensor range : Unspecified
+ Event Message Control : Per-threshold
+ Readable Thresholds : lcr lnc
+ Settable Thresholds : lcr lnc
+ Threshold Read Mask : lcr lnc
+ Assertion Events :
+ Assertions Enabled : lnc- lcr-
+ Deassertions Enabled : lnc- lcr-
+
+"""
+
+
+sensor_status_cmd = 'ipmitoolraw0x0a0x2c0x00'
+init_sensor_cmd = 'ipmitoolraw0x0a0x2c0x01'
+sdr_dump_cmd = 'ipmitoolsdrdump'
+sdr_info_cmd = 'ipmitoolsdrinfo'
+
+read_sensor_all_cmd = 'ipmitoolsdr-v'
+read_sensor_temperature_cmd = 'ipmitoolsdr-vtypeTemperature'
+read_sensor_voltage_cmd = 'ipmitoolsdr-vtypeVoltage'
+read_sensor_current_cmd = 'ipmitoolsdr-vtypeCurrent'
+read_sensor_fan_cmd = 'ipmitoolsdr-vtypeFan'
+
+device_id_cmd = 'ipmitoolraw0x060x01'
+nm_device_id_cmd = 'ipmitool-b0x6-t0x2craw0x060x01'
+get_power_cmd = 'ipmitool-b0x6-t0x2craw0x2e0xc80x570x010x000x010x000x00'
+get_temperature_cmd = 'ipmitool-b0x6-t0x2craw0x2e0xc80x570x010x000x020x000x00'
+
+
+device_id = (' 21 01 01 04 02 bf 57 01 00 49 00 01 07 50 0b', '')
+nm_device_id = (' 50 01 02 15 02 21 57 01 00 02 0b 02 09 10 01', '')
+
+# start from byte 3, get cur- 57 00(87), min- 03 00(3)
+# max- 37 02(567), avg- 5c 00(92)
+power_data = (' 57 01 00 57 00 03 00 37 02 5c 00 cc 37 f4 53 ce\n'
+ ' 9b 12 01 50\n', '')
+
+# start from byte 3, get cur- 17 00(23), min- 16 00(22)
+# max- 18 00(24), avg- 17 00(23)
+temperature_data = (' 57 01 00 17 00 16 00 18 00 17 00 f3 6f fe 53 85\n'
+ ' b7 02 00 50\n', '')
+
+sdr_info = ('', '')
+
+sensor_temperature = (sensor_temperature_data, '')
+sensor_voltage = (sensor_voltage_data, '')
+sensor_current = (sensor_current_data, '')
+sensor_fan = (sensor_fan_data, '')
diff --git a/ceilometer/tests/ipmi/platform/test_intel_node_manager.py b/ceilometer/tests/ipmi/platform/test_intel_node_manager.py
new file mode 100644
index 00000000..e795332d
--- /dev/null
+++ b/ceilometer/tests/ipmi/platform/test_intel_node_manager.py
@@ -0,0 +1,84 @@
+# Copyright 2014 Intel Corp.
+#
+# 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 mock
+
+from ceilometer.ipmi.platform import intel_node_manager as node_manager
+from ceilometer.tests.ipmi.platform import fake_utils
+from ceilometer import utils
+
+from oslotest import base
+
+
+class TestNodeManager(base.BaseTestCase):
+
+ def setUp(self):
+ super(TestNodeManager, self).setUp()
+
+ utils.execute = mock.Mock(side_effect=fake_utils.execute_with_nm)
+ self.nm = node_manager.NodeManager()
+
+ def test_read_power_all(self):
+ power = self.nm.read_power_all()
+
+ avg_val = node_manager._hex(power["Average_value"])
+ max_val = node_manager._hex(power["Maximum_value"])
+ min_val = node_manager._hex(power["Minimum_value"])
+ cur_val = node_manager._hex(power["Current_value"])
+
+ self.assertTrue(self.nm.nm_support)
+ # see ipmi_test_data.py for raw data
+ self.assertEqual(87, cur_val)
+ self.assertEqual(3, min_val)
+ self.assertEqual(567, max_val)
+ self.assertEqual(92, avg_val)
+
+ def test_read_temperature_all(self):
+ temperature = self.nm.read_temperature_all()
+
+ avg_val = node_manager._hex(temperature["Average_value"])
+ max_val = node_manager._hex(temperature["Maximum_value"])
+ min_val = node_manager._hex(temperature["Minimum_value"])
+ cur_val = node_manager._hex(temperature["Current_value"])
+
+ self.assertTrue(self.nm.nm_support)
+ # see ipmi_test_data.py for raw data
+ self.assertEqual(23, cur_val)
+ self.assertEqual(22, min_val)
+ self.assertEqual(24, max_val)
+ self.assertEqual(23, avg_val)
+
+
+class TestNonNodeManager(base.BaseTestCase):
+
+ def setUp(self):
+ super(TestNonNodeManager, self).setUp()
+
+ utils.execute = mock.Mock(side_effect=fake_utils.execute_without_nm)
+ self.nm = node_manager.NodeManager()
+ self.nm.nm_support = False
+
+ def test_read_power_all(self):
+ power = self.nm.read_power_all()
+
+ # Non-Node Manager platform return empty data
+ self.assertTrue(power == {})
+
+ def test_read_temperature_all(self):
+ temperature = self.nm.read_temperature_all()
+
+ # Non-Node Manager platform return empty data
+ self.assertTrue(temperature == {})
diff --git a/ceilometer/tests/ipmi/platform/test_ipmi_sensor.py b/ceilometer/tests/ipmi/platform/test_ipmi_sensor.py
new file mode 100644
index 00000000..3d5deca5
--- /dev/null
+++ b/ceilometer/tests/ipmi/platform/test_ipmi_sensor.py
@@ -0,0 +1,118 @@
+# Copyright 2014 Intel Corp.
+#
+# 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 mock
+
+from ceilometer.ipmi.platform import ipmi_sensor
+from ceilometer.tests.ipmi.platform import fake_utils
+from ceilometer import utils
+
+from oslotest import base
+
+
+class TestIPMISensor(base.BaseTestCase):
+
+ def setUp(self):
+ super(TestIPMISensor, self).setUp()
+
+ utils.execute = mock.Mock(side_effect=fake_utils.execute_with_nm)
+ self.ipmi = ipmi_sensor.IPMISensor()
+
+ def test_read_sensor_temperature(self):
+ sensors = self.ipmi.read_sensor_any('Temperature')
+
+ # only temperature data returned.
+ self.assertTrue('Temperature' in sensors)
+ self.assertEqual(1, len(sensors))
+
+ # 4 sensor data in total, ignore 1 without 'Sensor Reading'.
+ # Check ceilometer/tests/ipmi/platform/ipmi_test_data.py
+ self.assertEqual(3, len(sensors['Temperature']))
+ sensor = sensors['Temperature']['BB P1 VR Temp (0x20)']
+ self.assertEqual('25 (+/- 0) degrees C', sensor['Sensor Reading'])
+
+ def test_read_sensor_voltage(self):
+ sensors = self.ipmi.read_sensor_any('Voltage')
+
+ # only voltage data returned.
+ self.assertTrue('Voltage' in sensors)
+ self.assertEqual(1, len(sensors))
+
+ # 4 sensor data in total, ignore 1 without 'Sensor Reading'.
+ # Check ceilometer/tests/ipmi/platform/ipmi_test_data.py
+ self.assertEqual(3, len(sensors['Voltage']))
+ sensor = sensors['Voltage']['BB +5.0V (0xd1)']
+ self.assertEqual('4.959 (+/- 0) Volts', sensor['Sensor Reading'])
+
+ def test_read_sensor_current(self):
+ sensors = self.ipmi.read_sensor_any('Current')
+
+ # only Current data returned.
+ self.assertTrue('Current' in sensors)
+ self.assertEqual(1, len(sensors))
+
+ # 2 sensor data in total.
+ # Check ceilometer/tests/ipmi/platform/ipmi_test_data.py
+ self.assertEqual(2, len(sensors['Current']))
+ sensor = sensors['Current']['PS1 Curr Out % (0x58)']
+ self.assertEqual('11 (+/- 0) unspecified', sensor['Sensor Reading'])
+
+ def test_read_sensor_fan(self):
+ sensors = self.ipmi.read_sensor_any('Fan')
+
+ # only Fan data returned.
+ self.assertTrue('Fan' in sensors)
+ self.assertEqual(1, len(sensors))
+
+ # 2 sensor data in total.
+ # Check ceilometer/tests/ipmi/platform/ipmi_test_data.py
+ self.assertEqual(4, len(sensors['Fan']))
+ sensor = sensors['Fan']['System Fan 2 (0x32)']
+ self.assertEqual('4704 (+/- 0) RPM', sensor['Sensor Reading'])
+
+
+class TestNonIPMISensor(base.BaseTestCase):
+
+ def setUp(self):
+ super(TestNonIPMISensor, self).setUp()
+
+ utils.execute = mock.Mock(side_effect=fake_utils.execute_without_ipmi)
+ self.ipmi = ipmi_sensor.IPMISensor()
+ self.ipmi.ipmi_support = False
+
+ def test_read_sensor_temperature(self):
+ sensors = self.ipmi.read_sensor_any('Temperature')
+
+ # Non-IPMI platform return empty data
+ self.assertTrue(sensors == {})
+
+ def test_read_sensor_voltage(self):
+ sensors = self.ipmi.read_sensor_any('Voltage')
+
+ # Non-IPMI platform return empty data
+ self.assertTrue(sensors == {})
+
+ def test_read_sensor_current(self):
+ sensors = self.ipmi.read_sensor_any('Current')
+
+ # Non-IPMI platform return empty data
+ self.assertTrue(sensors == {})
+
+ def test_read_sensor_fan(self):
+ sensors = self.ipmi.read_sensor_any('Fan')
+
+ # Non-IPMI platform return empty data
+ self.assertTrue(sensors == {})
diff --git a/ceilometer/utils.py b/ceilometer/utils.py
index 5f05c3a0..c703521b 100644
--- a/ceilometer/utils.py
+++ b/ceilometer/utils.py
@@ -27,11 +27,32 @@ import hashlib
import multiprocessing
import struct
+from ceilometer.openstack.common import processutils
+from oslo.config import cfg
from oslo.utils import timeutils
from oslo.utils import units
import six
+rootwrap_conf = cfg.StrOpt('rootwrap_config',
+ default="/etc/ceilometer/rootwrap.conf",
+ help='Path to the rootwrap configuration file to'
+ 'use for running commands as root')
+CONF = cfg.CONF
+CONF.register_opt(rootwrap_conf)
+
+
+def _get_root_helper():
+ return 'sudo ceilometer-rootwrap %s' % CONF.rootwrap_config
+
+
+def execute(*cmd, **kwargs):
+ """Convenience wrapper around oslo's execute() method."""
+ if 'run_as_root' in kwargs and 'root_helper' not in kwargs:
+ kwargs['root_helper'] = _get_root_helper()
+ return processutils.execute(*cmd, **kwargs)
+
+
def recursive_keypairs(d, separator=':'):
"""Generator that produces sequence of keypairs for nested dictionaries."""
for name, value in sorted(six.iteritems(d)):
diff --git a/etc/ceilometer/rootwrap.conf b/etc/ceilometer/rootwrap.conf
new file mode 100644
index 00000000..c79065c7
--- /dev/null
+++ b/etc/ceilometer/rootwrap.conf
@@ -0,0 +1,27 @@
+# Configuration for ceilometer-rootwrap
+# This file should be owned by (and only-writeable by) the root user
+
+[DEFAULT]
+# List of directories to load filter definitions from (separated by ',').
+# These directories MUST all be only writeable by root !
+filters_path=/etc/ceilometer/rootwrap.d,/usr/share/ceilometer/rootwrap
+
+# List of directories to search executables in, in case filters do not
+# explicitely specify a full path (separated by ',')
+# If not specified, defaults to system PATH environment variable.
+# These directories MUST all be only writeable by root !
+exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin
+
+# Enable logging to syslog
+# Default value is False
+use_syslog=False
+
+# Which syslog facility to use.
+# Valid values include auth, authpriv, syslog, user0, user1...
+# Default value is 'syslog'
+syslog_log_facility=syslog
+
+# Which messages to log.
+# INFO means log all usage
+# ERROR means only log unsuccessful attempts
+syslog_log_level=ERROR
diff --git a/etc/ceilometer/rootwrap.d/ipmi.filters b/etc/ceilometer/rootwrap.d/ipmi.filters
new file mode 100644
index 00000000..2ef74b04
--- /dev/null
+++ b/etc/ceilometer/rootwrap.d/ipmi.filters
@@ -0,0 +1,7 @@
+# ceilometer-rootwrap command filters for IPMI capable nodes
+# This file should be owned by (and only-writeable by) the root user
+
+[Filters]
+# ceilometer/ipmi/nodemanager/node_manager.py: 'ipmitool'
+ipmitool: CommandFilter, ipmitool, root
+
diff --git a/openstack-common.conf b/openstack-common.conf
index 0cc63dd1..b6e337b2 100644
--- a/openstack-common.conf
+++ b/openstack-common.conf
@@ -10,6 +10,7 @@ module=log
module=log_handler
module=loopingcall
module=policy
+module=processutils
module=service
module=threadgroup
diff --git a/requirements.txt b/requirements.txt
index 5d75a5e1..1663de1f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,8 +16,10 @@ lockfile>=0.8
lxml>=2.3
msgpack-python>=0.4.0
netaddr>=0.7.6
+ordereddict
oslo.db>=0.4.0
oslo.config>=1.4.0.0a3
+oslo.rootwrap>=1.3.0.0a1
oslo.vmware>=0.5 # Apache-2.0
PasteDeploy>=1.5.0
pbr>=0.6,!=0.7,<1.0
diff --git a/setup.cfg b/setup.cfg
index fa8a76a4..812482f6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -263,6 +263,7 @@ console_scripts =
ceilometer-send-sample = ceilometer.cli:send_sample
ceilometer-dbsync = ceilometer.cmd.storage:dbsync
ceilometer-expirer = ceilometer.cmd.storage:expirer
+ ceilometer-rootwrap = oslo.rootwrap.cmd:main
ceilometer-collector = ceilometer.cmd.collector:main
ceilometer-alarm-evaluator = ceilometer.cmd.alarm:evaluator
ceilometer-alarm-notifier = ceilometer.cmd.alarm:notifier