From 1081ac1b6e32fe62c23d8b899527a7cede04b526 Mon Sep 17 00:00:00 2001 From: Edwin Zhai Date: Sat, 6 Sep 2014 00:01:45 +0800 Subject: [PATCH] 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 --- ceilometer/ipmi/__init__.py | 0 ceilometer/ipmi/platform/__init__.py | 0 ceilometer/ipmi/platform/exception.py | 24 ++ .../ipmi/platform/intel_node_manager.py | 274 +++++++++++++ ceilometer/ipmi/platform/ipmi_sensor.py | 115 ++++++ ceilometer/ipmi/platform/ipmitool.py | 132 +++++++ ceilometer/openstack/common/processutils.py | 285 ++++++++++++++ ceilometer/tests/ipmi/__init__.py | 0 ceilometer/tests/ipmi/platform/__init__.py | 0 ceilometer/tests/ipmi/platform/fake_utils.py | 98 +++++ .../tests/ipmi/platform/ipmitool_test_data.py | 359 ++++++++++++++++++ .../ipmi/platform/test_intel_node_manager.py | 84 ++++ .../tests/ipmi/platform/test_ipmi_sensor.py | 118 ++++++ ceilometer/utils.py | 21 + etc/ceilometer/rootwrap.conf | 27 ++ etc/ceilometer/rootwrap.d/ipmi.filters | 7 + openstack-common.conf | 1 + requirements.txt | 2 + setup.cfg | 1 + 19 files changed, 1548 insertions(+) create mode 100644 ceilometer/ipmi/__init__.py create mode 100644 ceilometer/ipmi/platform/__init__.py create mode 100644 ceilometer/ipmi/platform/exception.py create mode 100644 ceilometer/ipmi/platform/intel_node_manager.py create mode 100644 ceilometer/ipmi/platform/ipmi_sensor.py create mode 100644 ceilometer/ipmi/platform/ipmitool.py create mode 100644 ceilometer/openstack/common/processutils.py create mode 100644 ceilometer/tests/ipmi/__init__.py create mode 100644 ceilometer/tests/ipmi/platform/__init__.py create mode 100644 ceilometer/tests/ipmi/platform/fake_utils.py create mode 100644 ceilometer/tests/ipmi/platform/ipmitool_test_data.py create mode 100644 ceilometer/tests/ipmi/platform/test_intel_node_manager.py create mode 100644 ceilometer/tests/ipmi/platform/test_ipmi_sensor.py create mode 100644 etc/ceilometer/rootwrap.conf create mode 100644 etc/ceilometer/rootwrap.d/ipmi.filters diff --git a/ceilometer/ipmi/__init__.py b/ceilometer/ipmi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ceilometer/ipmi/platform/__init__.py b/ceilometer/ipmi/platform/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ceilometer/ipmi/platform/exception.py b/ceilometer/ipmi/platform/exception.py new file mode 100644 index 0000000000..0a7d48557f --- /dev/null +++ b/ceilometer/ipmi/platform/exception.py @@ -0,0 +1,24 @@ +# Copyright 2014 Intel Corporation. +# All Rights Reserved. +# +# Author: Zhai Edwin +# +# 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 0000000000..97ca0c1689 --- /dev/null +++ b/ceilometer/ipmi/platform/intel_node_manager.py @@ -0,0 +1,274 @@ +# Copyright 2014 Intel Corporation. +# All Rights Reserved. +# +# Author: Zhai Edwin +# Author: Gao Fengqian +# +# 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 0000000000..9749c48542 --- /dev/null +++ b/ceilometer/ipmi/platform/ipmi_sensor.py @@ -0,0 +1,115 @@ +# Copyright 2014 Intel Corporation. +# +# Author: Zhai Edwin +# +# 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 0000000000..a222b5f03c --- /dev/null +++ b/ceilometer/ipmi/platform/ipmitool.py @@ -0,0 +1,132 @@ +# Copyright 2014 Intel Corp. +# +# Author: Zhai Edwin +# Author: whaom +# +# 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 0000000000..ebb3cb7490 --- /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 0000000000..e69de29bb2 diff --git a/ceilometer/tests/ipmi/platform/__init__.py b/ceilometer/tests/ipmi/platform/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ceilometer/tests/ipmi/platform/fake_utils.py b/ceilometer/tests/ipmi/platform/fake_utils.py new file mode 100644 index 0000000000..d5fe47fb65 --- /dev/null +++ b/ceilometer/tests/ipmi/platform/fake_utils.py @@ -0,0 +1,98 @@ +# Copyright 2014 Intel Corp. +# +# Author: Zhai Edwin +# +# 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 0000000000..b64580e9a9 --- /dev/null +++ b/ceilometer/tests/ipmi/platform/ipmitool_test_data.py @@ -0,0 +1,359 @@ +# Copyright 2014 Intel Corp. +# +# Author: Zhai Edwin +# +# 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 0000000000..e795332d85 --- /dev/null +++ b/ceilometer/tests/ipmi/platform/test_intel_node_manager.py @@ -0,0 +1,84 @@ +# Copyright 2014 Intel Corp. +# +# Author: Zhai Edwin +# +# 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 0000000000..3d5deca53c --- /dev/null +++ b/ceilometer/tests/ipmi/platform/test_ipmi_sensor.py @@ -0,0 +1,118 @@ +# Copyright 2014 Intel Corp. +# +# Author: Zhai Edwin +# +# 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 5f05c3a00a..c703521bca 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 0000000000..c79065c764 --- /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 0000000000..2ef74b04ea --- /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 0cc63dd1ad..b6e337b232 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 5d75a5e189..1663de1fe7 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 fa8a76a4c3..812482f6f6 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