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
This commit is contained in:
Yadnesh Kulkarni 2023-06-23 10:27:19 +05:30 committed by Yadnesh Kulkarni
parent f1e6594e52
commit 8f54f95134
11 changed files with 166 additions and 14 deletions

View File

@ -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'

View File

@ -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]()

View File

@ -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:

View File

@ -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'

View File

@ -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
}
}
}

View File

@ -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.

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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__',

View File

@ -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