From 8f54f95134fccbd7b58c00a8d7499a68b71aef65 Mon Sep 17 00:00:00 2001 From: Yadnesh Kulkarni Date: Fri, 23 Jun 2023 10:27:19 +0530 Subject: [PATCH] Create new meter to poll power usage IPMI sensor 'Current' captures current & power consumption metrics. With the help of new pollster "hardware.ipmi.power", ceilometer ipmi agent can differentiate between current and power metrics as both are generated from the same sensor(Current). Power metrics are captured using a slightly different command than other sensors which is "ipmitool get sensor 'Pwr Consumption'". Closes-Bug: #2038425 Change-Id: I0a8af40626cd44dca9743fba63c8dbda8729d054 --- ceilometer/ipmi/notifications/ironic.py | 11 ++++ ceilometer/ipmi/platform/ipmi_sensor.py | 11 +++- ceilometer/ipmi/platform/ipmitool.py | 3 +- ceilometer/ipmi/pollsters/sensor.py | 13 +++++ .../unit/ipmi/notifications/ipmi_test_data.py | 55 +++++++++++++++++-- .../unit/ipmi/notifications/test_ironic.py | 27 ++++++++- .../unit/ipmi/platform/ipmitool_test_data.py | 20 +++++++ .../unit/ipmi/platform/test_ipmi_sensor.py | 17 +++++- .../tests/unit/ipmi/pollsters/test_sensor.py | 20 ++++++- ceilometer/tests/unit/polling/test_manager.py | 2 +- setup.cfg | 1 + 11 files changed, 166 insertions(+), 14 deletions(-) diff --git a/ceilometer/ipmi/notifications/ironic.py b/ceilometer/ipmi/notifications/ironic.py index 7b6c0f885d..c19ac31fc1 100644 --- a/ceilometer/ipmi/notifications/ironic.py +++ b/ceilometer/ipmi/notifications/ironic.py @@ -115,6 +115,13 @@ class SensorNotification(endpoint.SampleEndpoint): except KeyError as exc: raise InvalidSensorData('missing key in payload: %s' % exc) + # Do not pick up power consumption metrics from Current sensor + if ( + self.metric == 'Current' and + 'Pwr Consumption' in payload['Sensor ID'] + ): + continue + info = self._package_payload(message, payload) try: @@ -159,3 +166,7 @@ class FanSensorNotification(SensorNotification): class VoltageSensorNotification(SensorNotification): metric = 'Voltage' + + +class PowerSensorNotification(SensorNotification): + metric = 'Power' diff --git a/ceilometer/ipmi/platform/ipmi_sensor.py b/ceilometer/ipmi/platform/ipmi_sensor.py index e6d32f19c9..83eb15abdf 100644 --- a/ceilometer/ipmi/platform/ipmi_sensor.py +++ b/ceilometer/ipmi/platform/ipmi_sensor.py @@ -24,7 +24,8 @@ IPMICMD = {"sdr_dump": "sdr dump", "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"} + "sensor_dump_voltage": "sdr -v type Voltage", + "sensor_dump_power": "sensor get 'Pwr Consumption'"} # Requires translation of output into dict DICT_TRANSLATE_TEMPLATE = {"translate": 1} @@ -74,6 +75,11 @@ class IPMISensor(object): """Get the sensor data for Voltage.""" return IPMICMD['sensor_dump_voltage'] + @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE) + def _read_sensor_power(self): + """Get the sensor data for Power.""" + return IPMICMD['sensor_dump_power'] + @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE) def _read_sensor_current(self): """Get the sensor data for Current.""" @@ -93,7 +99,8 @@ class IPMISensor(object): 'Temperature': self._read_sensor_temperature, 'Fan': self._read_sensor_fan, 'Voltage': self._read_sensor_voltage, - 'Current': self._read_sensor_current} + 'Current': self._read_sensor_current, + 'Power': self._read_sensor_power} try: return mapping[sensor_type]() diff --git a/ceilometer/ipmi/platform/ipmitool.py b/ceilometer/ipmi/platform/ipmitool.py index cad17222f4..985614419a 100644 --- a/ceilometer/ipmi/platform/ipmitool.py +++ b/ceilometer/ipmi/platform/ipmitool.py @@ -19,6 +19,7 @@ from ceilometer.i18n import _ from ceilometer.ipmi.platform import exception as ipmiexcept import ceilometer.privsep.ipmitool +import shlex # Following 2 functions are copied from ironic project to handle ipmitool's @@ -122,7 +123,7 @@ def execute_ipmi_cmd(template=None): def _execute(self, **kwargs): args = ['ipmitool'] command = f(self, **kwargs) - args.extend(command.split(" ")) + args.extend(shlex.split(command)) try: (out, __) = ceilometer.privsep.ipmitool.ipmi(*args) except processutils.ProcessExecutionError: diff --git a/ceilometer/ipmi/pollsters/sensor.py b/ceilometer/ipmi/pollsters/sensor.py index 7b534faf01..19d11d4fb3 100644 --- a/ceilometer/ipmi/pollsters/sensor.py +++ b/ceilometer/ipmi/pollsters/sensor.py @@ -47,6 +47,11 @@ class SensorPollster(plugin_base.PollsterBase): @staticmethod def _get_sensor_types(data, sensor_type): + # Ipmitool reports 'Pwr Consumption' as sensor type 'Current'. + # Set sensor_type to 'Current' when polling 'Power' metrics. + if sensor_type == 'Power': + sensor_type = 'Current' + try: return (sensor_type_data for _, sensor_type_data in data[sensor_type].items()) @@ -81,6 +86,10 @@ class SensorPollster(plugin_base.PollsterBase): except KeyError: continue + # Do not pick up power consumption metrics from 'Current' sensor + if self.METRIC == 'Current' and 'Pwr Consumption' in sensor_id: + continue + if not parser.validate_reading(sensor_reading): continue @@ -123,3 +132,7 @@ class FanSensorPollster(SensorPollster): class VoltageSensorPollster(SensorPollster): METRIC = 'Voltage' + + +class PowerSensorPollster(SensorPollster): + METRIC = 'Power' diff --git a/ceilometer/tests/unit/ipmi/notifications/ipmi_test_data.py b/ceilometer/tests/unit/ipmi/notifications/ipmi_test_data.py index b1bac54452..f5ed8159ec 100644 --- a/ceilometer/tests/unit/ipmi/notifications/ipmi_test_data.py +++ b/ceilometer/tests/unit/ipmi/notifications/ipmi_test_data.py @@ -273,9 +273,9 @@ TEMPERATURE_DATA = { CURRENT_DATA = { - 'Avg Power (0x2e)': { + 'Current 1 (0x6b)': { 'Status': 'ok', - 'Sensor Reading': '130 (+/- 0) Watts', + 'Sensor Reading': '0.800 (+/- 0) Amps', 'Entity ID': '21.0 (Power Management)', 'Assertions Enabled': '', 'Event Message Control': 'Per-threshold', @@ -284,10 +284,56 @@ CURRENT_DATA = { 'Sensor Type (Analog)': 'Current', 'Negative Hysteresis': 'Unspecified', 'Maximum sensor range': 'Unspecified', - 'Sensor ID': 'Avg Power (0x2e)', + 'Sensor ID': 'Current 1 (0x6b)', 'Assertion Events': '', 'Minimum sensor range': '2550.000', 'Settable Thresholds': 'No Thresholds' + }, + 'Pwr Consumption (0x76)': { + 'Entity ID': '7.1 (System Board)', + 'Sensor Type (Threshold)': 'Current (0x03)', + 'Sensor Reading': '160 (+/- 0) Watts', + 'Status': 'ok', + 'Nominal Reading': '1034.000', + 'Normal Maximum': '1056.000', + 'Upper critical': '1914.000', + 'Upper non-critical': '1738.000', + 'Positive Hysteresis': 'Unspecified', + 'Negative Hysteresis': 'Unspecified', + 'Minimum sensor range': 'Unspecified', + 'Maximum sensor range': '5588.000', + 'Sensor ID': 'Pwr Consumption (0x76)', + 'Event Message Control': 'Per-threshold', + 'Readable Thresholds': 'unc ucr', + 'Settable Thresholds': 'unc', + 'Assertion Events': '', + 'Assertions Enabled': 'unc+ ucr+', + 'Deassertions Enabled': 'unc+ ucr+' + } +} + + +POWER_DATA = { + 'Pwr Consumption (0x76)': { + 'Entity ID': '7.1 (System Board)', + 'Sensor Type (Threshold)': 'Current (0x03)', + 'Sensor Reading': '154 (+/- 0) Watts', + 'Status': 'ok', + 'Nominal Reading': '1034.000', + 'Normal Maximum': '1056.000', + 'Upper critical': '1914.000', + 'Upper non-critical': '1738.000', + 'Positive Hysteresis': 'Unspecified', + 'Negative Hysteresis': 'Unspecified', + 'Minimum sensor range': 'Unspecified', + 'Maximum sensor range': '5588.000', + 'Sensor ID': 'Pwr Consumption (0x76)', + 'Event Message Control': 'Per-threshold', + 'Readable Thresholds': 'unc ucr', + 'Settable Thresholds': 'unc', + 'Assertion Events': '', + 'Assertions Enabled': 'unc+ ucr+', + 'Deassertions Enabled': 'unc+ ucr+' } } @@ -661,7 +707,8 @@ SENSOR_DATA = { 'Temperature': TEMPERATURE_DATA, 'Current': CURRENT_DATA, 'Fan': FAN_DATA, - 'Voltage': VOLTAGE_DATA + 'Voltage': VOLTAGE_DATA, + 'Power': POWER_DATA } } } diff --git a/ceilometer/tests/unit/ipmi/notifications/test_ironic.py b/ceilometer/tests/unit/ipmi/notifications/test_ironic.py index 696748f00a..a50d8d54d9 100644 --- a/ceilometer/tests/unit/ipmi/notifications/test_ironic.py +++ b/ceilometer/tests/unit/ipmi/notifications/test_ironic.py @@ -71,14 +71,35 @@ class TestNotifications(base.BaseTestCase): self.assertEqual(1, len(counters), 'expected 1 current reading') resource_id = ( - 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-avg_power_(0x2e)' + 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-current_1_(0x6b)' ) test_counter = counters[resource_id] - self.assertEqual(130.0, test_counter.volume) - self.assertEqual('W', test_counter.unit) + self.assertEqual(0.800, test_counter.volume) + self.assertEqual('Amps', test_counter.unit) self.assertEqual(sample.TYPE_GAUGE, test_counter.type) self.assertEqual('hardware.ipmi.current', test_counter.name) + def test_ipmi_power_notification(self): + """Test IPMI Power sample from Current sensor. + + A single power reading is effectively the same as temperature, + modulo "power". + """ + processor = ipmi.PowerSensorNotification(None, None) + counters = dict([(counter.resource_id, counter) for counter in + processor.build_sample( + ipmi_test_data.SENSOR_DATA)]) + + self.assertEqual(1, len(counters), 'expected 1 current reading') + resource_id = ( + 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-pwr_consumption_(0x76)' + ) + test_counter = counters[resource_id] + self.assertEqual(154, test_counter.volume) + self.assertEqual('W', test_counter.unit) + self.assertEqual(sample.TYPE_GAUGE, test_counter.type) + self.assertEqual('hardware.ipmi.power', test_counter.name) + def test_ipmi_fan_notification(self): """Test IPMI Fan sensor data. diff --git a/ceilometer/tests/unit/ipmi/platform/ipmitool_test_data.py b/ceilometer/tests/unit/ipmi/platform/ipmitool_test_data.py index 7504aba3b0..0524a370ac 100644 --- a/ceilometer/tests/unit/ipmi/platform/ipmitool_test_data.py +++ b/ceilometer/tests/unit/ipmi/platform/ipmitool_test_data.py @@ -226,6 +226,26 @@ Sensor ID : PS2 Curr Out % (0x59) Assertions Enabled : unc+ ucr+ Deassertions Enabled : unc+ ucr+ +Sensor ID : Pwr Consumption (0x76) + Entity ID : 7.1 (System Board) + Sensor Type (Threshold) : Current (0x03) + Sensor Reading : 154 (+/- 0) Watts + Status : ok + Nominal Reading : 1034.000 + Normal Maximum : 1056.000 + Upper critical : 1914.000 + Upper non-critical : 1738.000 + Positive Hysteresis : Unspecified + Negative Hysteresis : Unspecified + Minimum sensor range : Unspecified + Maximum sensor range : 5588.000 + Event Message Control : Per-threshold + Readable Thresholds : unc ucr + Settable Thresholds : unc + Assertion Events : + Assertions Enabled : unc+ ucr+ + Deassertions Enabled : unc+ ucr+ + """ sensor_fan_data = """Sensor ID : System Fan 1 (0x30) diff --git a/ceilometer/tests/unit/ipmi/platform/test_ipmi_sensor.py b/ceilometer/tests/unit/ipmi/platform/test_ipmi_sensor.py index 21a1b11331..9e82e821fe 100644 --- a/ceilometer/tests/unit/ipmi/platform/test_ipmi_sensor.py +++ b/ceilometer/tests/unit/ipmi/platform/test_ipmi_sensor.py @@ -69,12 +69,25 @@ class TestIPMISensor(base.BaseTestCase): self.assertIn('Current', sensors) self.assertEqual(1, len(sensors)) - # 2 sensor data in total. + # 3 sensor data in total. # Check ceilometer/tests/ipmi/platform/ipmi_test_data.py - self.assertEqual(2, len(sensors['Current'])) + self.assertEqual(3, len(sensors['Current'])) sensor = sensors['Current']['PS1 Curr Out % (0x58)'] self.assertEqual('11 (+/- 0) unspecified', sensor['Sensor Reading']) + def test_read_sensor_power(self): + sensors = self.ipmi.read_sensor_any('Current') + + # only Current data returned. + self.assertIn('Current', sensors) + self.assertEqual(1, len(sensors)) + + # 3 sensor data in total. + # Check ceilometer/tests/ipmi/platform/ipmi_test_data.py + self.assertEqual(3, len(sensors['Current'])) + sensor = sensors['Current']['Pwr Consumption (0x76)'] + self.assertEqual('154 (+/- 0) Watts', sensor['Sensor Reading']) + def test_read_sensor_fan(self): sensors = self.ipmi.read_sensor_any('Fan') diff --git a/ceilometer/tests/unit/ipmi/pollsters/test_sensor.py b/ceilometer/tests/unit/ipmi/pollsters/test_sensor.py index 8a79ffbbad..471157192f 100644 --- a/ceilometer/tests/unit/ipmi/pollsters/test_sensor.py +++ b/ceilometer/tests/unit/ipmi/pollsters/test_sensor.py @@ -32,6 +32,10 @@ VOLTAGE_SENSOR_DATA = { 'Voltage': ipmi_test_data.VOLTAGE_DATA } +POWER_SENSOR_DATA = { + 'Current': ipmi_test_data.POWER_DATA +} + MISSING_SENSOR_DATA = ipmi_test_data.MISSING_SENSOR['payload']['payload'] MALFORMED_SENSOR_DATA = ipmi_test_data.BAD_SENSOR['payload']['payload'] MISSING_ID_SENSOR_DATA = ipmi_test_data.NO_SENSOR_ID['payload']['payload'] @@ -115,7 +119,7 @@ class TestCurrentSensorPollster(base.TestPollsterBase): def test_get_samples(self): self._test_get_samples() - self._verify_metering(1, float(130), self.CONF.host) + self._verify_metering(1, float(0.800), self.CONF.host) class TestVoltageSensorPollster(base.TestPollsterBase): @@ -130,3 +134,17 @@ class TestVoltageSensorPollster(base.TestPollsterBase): self._test_get_samples() self._verify_metering(4, float(3.309), self.CONF.host) + + +class TestPowerSensorPollster(base.TestPollsterBase): + + def fake_sensor_data(self, sensor_type): + return POWER_SENSOR_DATA + + def make_pollster(self): + return sensor.PowerSensorPollster(self.CONF) + + def test_get_samples(self): + self._test_get_samples() + + self._verify_metering(1, int(154), self.CONF.host) diff --git a/ceilometer/tests/unit/polling/test_manager.py b/ceilometer/tests/unit/polling/test_manager.py index a2bce25955..1a2b7b33f6 100644 --- a/ceilometer/tests/unit/polling/test_manager.py +++ b/ceilometer/tests/unit/polling/test_manager.py @@ -102,7 +102,7 @@ class TestManager(base.BaseTestCase): mgr = manager.AgentManager(0, self.conf, namespaces=['ipmi']) # 8 pollsters for Node Manager - self.assertEqual(12, len(mgr.extensions)) + self.assertEqual(13, len(mgr.extensions)) # Skip loading pollster upon ExtensionLoadError @mock.patch('ceilometer.ipmi.pollsters.node._Base.__init__', diff --git a/setup.cfg b/setup.cfg index 2c5890916b..30079a323b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -115,6 +115,7 @@ ceilometer.poll.ipmi = hardware.ipmi.voltage = ceilometer.ipmi.pollsters.sensor:VoltageSensorPollster hardware.ipmi.current = ceilometer.ipmi.pollsters.sensor:CurrentSensorPollster hardware.ipmi.fan = ceilometer.ipmi.pollsters.sensor:FanSensorPollster + hardware.ipmi.power = ceilometer.ipmi.pollsters.sensor:PowerSensorPollster ceilometer.poll.central = ip.floating = ceilometer.network.floatingip:FloatingIPPollster