From c389fccfcf18983e47fbd595dccec0a35fb39293 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Mon, 25 May 2015 20:56:09 +0000 Subject: [PATCH] Add sensors plugin This plugin uses the lm_sensors library to monitor all sorts of internal sensors of a system. Those could be temperature sensors, fan sensors or voltage sensors. --- Makefile.am | 6 +- configure.ac | 10 + src/_collectymodule.c | 417 +++++++++++++++++++++++++++++++ src/collecty/plugins/__init__.py | 1 + src/collecty/plugins/sensors.py | 343 +++++++++++++++++++++++++ 5 files changed, 775 insertions(+), 2 deletions(-) create mode 100644 src/collecty/plugins/sensors.py diff --git a/Makefile.am b/Makefile.am index b9aa841..98ac26d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -93,7 +93,8 @@ collectyplugins_PYTHON = \ src/collecty/plugins/interface.py \ src/collecty/plugins/latency.py \ src/collecty/plugins/loadavg.py \ - src/collecty/plugins/memory.py + src/collecty/plugins/memory.py \ + src/collecty/plugins/sensors.py collectypluginsdir = $(collectydir)/plugins @@ -116,7 +117,8 @@ _collecty_la_LDFLAGS = \ _collecty_la_LIBADD = \ $(LIBATASMART_LIBS) \ - $(PYTHON_DEVEL_LIBS) + $(PYTHON_DEVEL_LIBS) \ + $(SENSORS_LIBS) dist_dbuspolicy_DATA = \ src/dbus/org.ipfire.collecty1.conf diff --git a/configure.ac b/configure.ac index 53616a0..8980c58 100644 --- a/configure.ac +++ b/configure.ac @@ -70,6 +70,16 @@ PKG_CHECK_MODULES([LIBATASMART], [libatasmart >= 0.19]) save_LIBS="$LIBS" +# lm-sensors +AC_CHECK_HEADERS([sensors/sensors.h sensors/errors.h]) + +LIBS= +AC_CHECK_LIB(sensors, sensors_init, [], [AC_MSG_ERROR([*** sensors library not found])]) +SENSORS_LIBS="$LIBS" +AC_SUBST(SENSORS_LIBS) + +LIBS="$save_LIBS" + # pkg-config PKG_PROG_PKG_CONFIG # This makes sure pkg.m4 is available. diff --git a/src/_collectymodule.c b/src/_collectymodule.c index eefe871..38ade7d 100644 --- a/src/_collectymodule.c +++ b/src/_collectymodule.c @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include #include #include @@ -311,7 +313,418 @@ static PyTypeObject BlockDeviceType = { BlockDevice_new, /* tp_new */ }; +typedef struct { + PyObject_HEAD + const sensors_chip_name* chip; + const sensors_feature* feature; +} SensorObject; + +static void Sensor_dealloc(SensorObject* self) { + self->ob_type->tp_free((PyObject*)self); +} + +static PyObject* Sensor_new(PyTypeObject* type, PyObject* args, PyObject* kwds) { + SensorObject* self = (SensorObject*)type->tp_alloc(type, 0); + + return (PyObject *)self; +} + +static int Sensor_init(SensorObject* self, PyObject* args, PyObject* kwds) { + return 0; +} + +static PyObject* Sensor_get_label(SensorObject* self) { + char* label = sensors_get_label(self->chip, self->feature); + + if (label) { + PyObject* string = PyString_FromString(label); + free(label); + + return string; + } + + Py_RETURN_NONE; +} + +static PyObject* Sensor_get_name(SensorObject* self) { + char chip_name[512]; + + int r = sensors_snprintf_chip_name(chip_name, sizeof(chip_name), self->chip); + if (r < 0) { + PyErr_Format(PyExc_RuntimeError, "Could not print chip name"); + return NULL; + } + + return PyString_FromString(chip_name); +} + +static PyObject* Sensor_get_type(SensorObject* self) { + const char* type = NULL; + + switch (self->feature->type) { + case SENSORS_FEATURE_IN: + type = "voltage"; + break; + + case SENSORS_FEATURE_FAN: + type = "fan"; + break; + + case SENSORS_FEATURE_TEMP: + type = "temperature"; + break; + + case SENSORS_FEATURE_POWER: + type = "power"; + break; + + default: + break; + } + + if (type) + return PyString_FromString(type); + + Py_RETURN_NONE; +} + +static PyObject* Sensor_get_bus(SensorObject* self) { + const char* type = NULL; + + switch (self->chip->bus.type) { + case SENSORS_BUS_TYPE_I2C: + type = "i2c"; + break; + + case SENSORS_BUS_TYPE_ISA: + type = "isa"; + break; + + case SENSORS_BUS_TYPE_PCI: + type = "pci"; + break; + + case SENSORS_BUS_TYPE_SPI: + type = "spi"; + break; + + case SENSORS_BUS_TYPE_VIRTUAL: + type = "virtual"; + break; + + case SENSORS_BUS_TYPE_ACPI: + type = "acpi"; + break; + + case SENSORS_BUS_TYPE_HID: + type = "hid"; + break; + + default: + break; + } + + if (type) + return PyString_FromString(type); + + Py_RETURN_NONE; +} + +static const sensors_subfeature* Sensor_get_subfeature(SensorObject* sensor, sensors_subfeature_type type) { + const sensors_subfeature* subfeature; + int subfeature_num = 0; + + while ((subfeature = sensors_get_all_subfeatures(sensor->chip, sensor->feature, &subfeature_num))) { + if (subfeature->type == type) + break; + } + + return subfeature; +} + +static PyObject* Sensor_return_value(SensorObject* sensor, sensors_subfeature_type subfeature_type) { + double value; + + const sensors_subfeature* subfeature = Sensor_get_subfeature(sensor, subfeature_type); + if (!subfeature) { + PyErr_Format(PyExc_AttributeError, "Could not find sensor of requested type"); + return NULL; + } + + // Fetch value from the sensor + int r = sensors_get_value(sensor->chip, subfeature->number, &value); + if (r < 0) { + PyErr_Format(PyExc_ValueError, "Error retrieving value from sensor: %s", + sensors_strerror(errno)); + return NULL; + } + + // Convert all temperature values from Celcius to Kelvon + if (sensor->feature->type == SENSORS_FEATURE_TEMP) + value += 273.15; + + return PyFloat_FromDouble(value); +} + +static PyObject* Sensor_no_value() { + PyErr_Format(PyExc_ValueError, "Value not supported for this sensor type"); + return NULL; +} + +static PyObject* Sensor_get_value(SensorObject* self) { + sensors_subfeature_type subfeature_type; + + switch (self->feature->type) { + case SENSORS_FEATURE_IN: + subfeature_type = SENSORS_SUBFEATURE_IN_INPUT; + break; + + case SENSORS_FEATURE_FAN: + subfeature_type = SENSORS_SUBFEATURE_FAN_INPUT; + break; + + case SENSORS_FEATURE_TEMP: + subfeature_type = SENSORS_SUBFEATURE_TEMP_INPUT; + break; + + case SENSORS_FEATURE_POWER: + subfeature_type = SENSORS_SUBFEATURE_POWER_INPUT; + break; + + default: + return Sensor_no_value(); + } + + return Sensor_return_value(self, subfeature_type); +} + +static PyObject* Sensor_get_critical(SensorObject* self) { + sensors_subfeature_type subfeature_type; + + switch (self->feature->type) { + case SENSORS_FEATURE_IN: + subfeature_type = SENSORS_SUBFEATURE_IN_CRIT; + break; + + case SENSORS_FEATURE_TEMP: + subfeature_type = SENSORS_SUBFEATURE_TEMP_CRIT; + break; + + case SENSORS_FEATURE_POWER: + subfeature_type = SENSORS_SUBFEATURE_POWER_CRIT; + break; + + default: + return Sensor_no_value(); + } + + return Sensor_return_value(self, subfeature_type); +} + +static PyObject* Sensor_get_maximum(SensorObject* self) { + sensors_subfeature_type subfeature_type; + + switch (self->feature->type) { + case SENSORS_FEATURE_IN: + subfeature_type = SENSORS_SUBFEATURE_IN_MAX; + break; + + case SENSORS_FEATURE_FAN: + subfeature_type = SENSORS_SUBFEATURE_FAN_MAX; + break; + + case SENSORS_FEATURE_TEMP: + subfeature_type = SENSORS_SUBFEATURE_TEMP_MAX; + break; + + case SENSORS_FEATURE_POWER: + subfeature_type = SENSORS_SUBFEATURE_POWER_MAX; + break; + + default: + return Sensor_no_value(); + } + + return Sensor_return_value(self, subfeature_type); +} + +static PyObject* Sensor_get_minimum(SensorObject* self) { + sensors_subfeature_type subfeature_type; + + switch (self->feature->type) { + case SENSORS_FEATURE_IN: + subfeature_type = SENSORS_SUBFEATURE_IN_MIN; + break; + + case SENSORS_FEATURE_FAN: + subfeature_type = SENSORS_SUBFEATURE_FAN_MIN; + break; + + case SENSORS_FEATURE_TEMP: + subfeature_type = SENSORS_SUBFEATURE_TEMP_MIN; + break; + + default: + return Sensor_no_value(); + } + + return Sensor_return_value(self, subfeature_type); +} + +static PyObject* Sensor_get_high(SensorObject* self) { + sensors_subfeature_type subfeature_type; + + switch (self->feature->type) { + case SENSORS_FEATURE_TEMP: + subfeature_type = SENSORS_SUBFEATURE_TEMP_MAX; + break; + + default: + return Sensor_no_value(); + } + + return Sensor_return_value(self, subfeature_type); +} + +static PyGetSetDef Sensor_getsetters[] = { + {"bus", (getter)Sensor_get_bus, NULL, NULL, NULL}, + {"critical", (getter)Sensor_get_critical, NULL, NULL, NULL}, + {"high", (getter)Sensor_get_high, NULL, NULL, NULL}, + {"label", (getter)Sensor_get_label, NULL, NULL, NULL}, + {"maximum", (getter)Sensor_get_maximum, NULL, NULL, NULL}, + {"minumum", (getter)Sensor_get_minimum, NULL, NULL, NULL}, + {"name", (getter)Sensor_get_name, NULL, NULL, NULL}, + {"type", (getter)Sensor_get_type, NULL, NULL, NULL}, + {"value", (getter)Sensor_get_value, NULL, NULL, NULL}, + {NULL}, +}; + +static PyTypeObject SensorType = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "_collecty.Sensor", /*tp_name*/ + sizeof(SensorObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)Sensor_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/ + "Sensor objects", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + Sensor_getsetters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)Sensor_init, /* tp_init */ + 0, /* tp_alloc */ + Sensor_new, /* tp_new */ +}; + +static SensorObject* make_sensor_object(const sensors_chip_name* chip, const sensors_feature* feature) { + SensorObject* sensor = PyObject_New(SensorObject, &SensorType); + if (!sensor) + return NULL; + + if (!PyObject_Init((PyObject*)sensor, &SensorType)) { + Py_DECREF(sensor); + return NULL; + } + + sensor->chip = chip; + sensor->feature = feature; + + return sensor; +} + +static PyObject* _collecty_sensors_init() { + // Clean up everything first in case sensors_init was called earlier + sensors_cleanup(); + + int r = sensors_init(NULL); + if (r) { + PyErr_Format(PyExc_OSError, "Could not initialise sensors: %s", + sensors_strerror(errno)); + return NULL; + } + + Py_RETURN_NONE; +} + +static PyObject* _collecty_sensors_cleanup() { + sensors_cleanup(); + Py_RETURN_NONE; +} + +static PyObject* _collecty_get_detected_sensors(PyObject* o, PyObject* args) { + const char* name = NULL; + sensors_chip_name chip_name; + + if (!PyArg_ParseTuple(args, "|z", &name)) + return NULL; + + if (name) { + int r = sensors_parse_chip_name(name, &chip_name); + if (r < 0) { + PyErr_Format(PyExc_ValueError, "Could not parse chip name: %s", name); + return NULL; + } + } + + PyObject* list = PyList_New(0); + + const sensors_chip_name* chip; + int chip_num = 0; + + while ((chip = sensors_get_detected_chips((name) ? &chip_name : NULL, &chip_num))) { + const sensors_feature* feature; + int feature_num = 0; + + while ((feature = sensors_get_features(chip, &feature_num))) { + // Skip sensors we do not want to support + switch (feature->type) { + case SENSORS_FEATURE_IN: + case SENSORS_FEATURE_FAN: + case SENSORS_FEATURE_TEMP: + case SENSORS_FEATURE_POWER: + break; + + default: + continue; + } + + SensorObject* sensor = make_sensor_object(chip, feature); + PyList_Append(list, (PyObject*)sensor); + } + } + + return list; +} + static PyMethodDef collecty_module_methods[] = { + {"get_detected_sensors", (PyCFunction)_collecty_get_detected_sensors, METH_VARARGS, NULL}, + {"sensors_cleanup", (PyCFunction)_collecty_sensors_cleanup, METH_NOARGS, NULL}, + {"sensors_init", (PyCFunction)_collecty_sensors_init, METH_NOARGS, NULL}, {NULL}, }; @@ -319,7 +732,11 @@ void init_collecty(void) { if (PyType_Ready(&BlockDeviceType) < 0) return; + if (PyType_Ready(&SensorType) < 0) + return; + PyObject* m = Py_InitModule("_collecty", collecty_module_methods); PyModule_AddObject(m, "BlockDevice", (PyObject*)&BlockDeviceType); + PyModule_AddObject(m, "Sensor", (PyObject*)&SensorType); } diff --git a/src/collecty/plugins/__init__.py b/src/collecty/plugins/__init__.py index 5f843a8..faa9348 100644 --- a/src/collecty/plugins/__init__.py +++ b/src/collecty/plugins/__init__.py @@ -30,3 +30,4 @@ import interface import latency import loadavg import memory +import sensors diff --git a/src/collecty/plugins/sensors.py b/src/collecty/plugins/sensors.py new file mode 100644 index 0000000..e4c247e --- /dev/null +++ b/src/collecty/plugins/sensors.py @@ -0,0 +1,343 @@ +#!/usr/bin/python +# encoding: utf-8 +############################################################################### +# # +# collecty - A system statistics collection daemon for IPFire # +# Copyright (C) 2015 IPFire development team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +############################################################################### + +from collecty import _collecty +import os +import re + +import base + +from ..i18n import _ + +class GraphTemplateSensorsTemperature(base.GraphTemplate): + name = "sensors-temperature" + + rrd_graph = [ + "DEF:value_kelvin=%(file)s:value:AVERAGE", + "DEF:critical_kelvin=%(file)s:critical:AVERAGE", + "DEF:high_kelvin=%(file)s:high:AVERAGE", + "DEF:low_kelvin=%(file)s:low:AVERAGE", + + # Convert everything to celsius + "CDEF:value=value_kelvin,273.15,-", + "CDEF:critical=critical_kelvin,273.15,-", + "CDEF:high=high_kelvin,273.15,-", + "CDEF:low=low_kelvin,273.15,-", + + # Change colour when the value gets above high + "CDEF:value_high=value,high,GT,value,UNKN,IF", + "CDEF:value_normal=value,high,GT,UNKN,value,IF", + + "VDEF:value_cur=value,LAST", + "VDEF:value_avg=value,AVERAGE", + "VDEF:value_max=value,MAXIMUM", + "VDEF:value_min=value,MINIMUM", + + # Get data points for the threshold lines + "VDEF:critical_line=critical,MINIMUM", + "VDEF:low_line=low,MAXIMUM", + + # Draw the temperature value + "LINE3:value_high#ff0000", + "LINE2:value_normal#00ff00:%-15s" % _("Temperature"), + + # Draw the legend + "GPRINT:value_cur:%%10.2lf °C\l", + "GPRINT:value_avg: %-15s %%6.2lf °C\l" % _("Average"), + "GPRINT:value_max: %-15s %%6.2lf °C\l" % _("Maximum"), + "GPRINT:value_min: %-15s %%6.2lf °C\l" % _("Minimum"), + + # Empty line + "COMMENT: \\n", + + # Draw boundary lines + "COMMENT:%s\:" % _("Temperature Thresholds"), + "HRULE:critical_line#000000:%-15s" % _("Critical"), + "GPRINT:critical_line:%%6.2lf °C\\r", + "HRULE:low_line#0000ff:%-15s" % _("Low"), + "GPRINT:low_line:%%6.2lf °C\\r", + ] + + @property + def graph_title(self): + return _("Temperature (%s)") % self.object.sensor.name + + @property + def graph_vertical_label(self): + return _("° Celsius") + + +class GraphTemplateSensorsProcessorTemperature(base.GraphTemplate): + name = "processor-temperature" + + core_colours = { + "core0" : "#ff000033", + "core1" : "#0000ff33", + "core2" : "#00ff0033", + "core3" : "#0000ff33", + } + + def get_temperature_sensors(self): + return self.plugin.get_detected_sensor_objects("coretemp-*") + + def get_object_table(self): + objects_table = {} + + counter = 0 + for object in self.get_temperature_sensors(): + objects_table["core%s" % counter] = object + counter += 1 + + return objects_table + + @property + def rrd_graph(self): + rrd_graph = [] + + cores = sorted(self.object_table.keys()) + + for core in cores: + i = { + "core" : core, + } + + core_graph = [ + "DEF:value_kelvin_%(core)s=%%(%(core)s)s:value:AVERAGE", + "DEF:critical_kelvin_%(core)s=%%(%(core)s)s:critical:AVERAGE", + "DEF:high_kelvin_%(core)s=%%(%(core)s)s:high:AVERAGE", + + # Convert everything to celsius + "CDEF:value_%(core)s=value_kelvin_%(core)s,273.15,-", + "CDEF:critical_%(core)s=critical_kelvin_%(core)s,273.15,-", + "CDEF:high_%(core)s=high_kelvin_%(core)s,273.15,-", + ] + + rrd_graph += [line % i for line in core_graph] + + all_core_values = ("value_%s" % c for c in cores) + all_core_highs = ("high_%s" % c for c in cores) + + rrd_graph += [ + # Compute the temperature of the processor + # by taking the average of all cores + "CDEF:value=%s,%s,AVG" % (",".join(all_core_values), len(cores)), + "CDEF:high=%s,MIN" % ",".join(all_core_highs), + + # Change colour when the value gets above high + "CDEF:value_high=value,high,GT,value,UNKN,IF", + "CDEF:value_normal=value,high,GT,UNKN,value,IF", + + "VDEF:value_avg=value,AVERAGE", + "VDEF:value_max=value,MAXIMUM", + "VDEF:value_min=value,MINIMUM", + + "LINE3:value_high#FF0000", + "LINE3:value_normal#000000:%-15s\l" % _("Temperature"), + + "GPRINT:value_avg: %-15s %%6.2lf °C\l" % _("Average"), + "GPRINT:value_max: %-15s %%6.2lf °C\l" % _("Maximum"), + "GPRINT:value_min: %-15s %%6.2lf °C\l" % _("Minimum"), + ] + + for core in cores: + object = self.object_table.get(core) + + i = { + "colour" : self.core_colours.get(core, "#000000"), + "core" : core, + "label" : object.sensor.label, + } + + core_graph = [ + # TODO these lines were supposed to be dashed, but that + # didn't really work here + "LINE2:value_%(core)s%(colour)s:%(label)-10s", + ] + + rrd_graph += [line % i for line in core_graph] + + # Draw the critical line + rrd_graph += [ + "VDEF:critical_line=critical_core0,MINIMUM", + "HRULE:critical_line#000000:%-15s" % _("Critical"), + "GPRINT:critical_line:%%6.2lf °C\\r", + ] + + return rrd_graph + + @property + def graph_title(self): + return _("Processor") + + @property + def graph_vertical_label(self): + return _("Temperature") + + +class SensorBaseObject(base.Object): + def init(self, sensor): + self.sensor = sensor + + def __repr__(self): + return "<%s %s (%s)>" % (self.__class__.__name__, self.sensor.name, self.sensor.label) + + @property + def id(self): + return "-".join((self.sensor.name, self.sensor.label)) + + @property + def type(self): + return self.sensor.type + + +class SensorTemperatureObject(SensorBaseObject): + rrd_schema = [ + "DS:value:GAUGE:0:U", + "DS:critical:GAUGE:0:U", + "DS:low:GAUGE:0:U", + "DS:high:GAUGE:0:U", + ] + + def collect(self): + assert self.type == "temperature" + + return ( + self.sensor.value, + self.critical, + self.low, + self.high, + ) + + @property + def critical(self): + try: + return self.sensor.critical + except AttributeError: + return "NaN" + + @property + def low(self): + try: + return self.sensor.minimum + except AttributeError: + return "NaN" + + @property + def high(self): + try: + return self.sensor.high + except AttributeError: + return "NaN" + + +class SensorVoltageObject(SensorBaseObject): + rrd_schema = [ + "DS:value:GAUGE:0:U", + "DS:minimum:GAUGE:0:U", + "DS:maximum:GAUGE:0:U", + ] + + def collect(self): + assert self.type == "voltage" + + return ( + self.sensor.value, + self.minimum, + self.maximum, + ) + + @property + def minimum(self): + try: + return self.sensor.minimum + except AttributeError: + return "NaN" + + @property + def maximum(self): + try: + return self.sensor.maximum + except AttributeError: + return "NaN" + + +class SensorFanObject(SensorBaseObject): + rrd_schema = [ + "DS:value:GAUGE:0:U", + "DS:minimum:GAUGE:0:U", + "DS:maximum:GAUGE:0:U", + ] + + def collect(self): + assert self.type == "fan" + + return ( + self.sensor.value, + self.minimum, + self.maximum, + ) + + @property + def mimimum(self): + try: + return self.sensor.minimum + except AttributeError: + return "NaN" + + @property + def maximum(self): + try: + return self.sensor.maximum + except AttributeError: + return "NaN" + + +class SensorsPlugin(base.Plugin): + name = "sensors" + description = "Sensors Plugin" + + templates = [ + GraphTemplateSensorsProcessorTemperature, + GraphTemplateSensorsTemperature, + ] + + def init(self): + # Initialise the sensors library. + _collecty.sensors_init() + + def __del__(self): + _collecty.sensors_cleanup() + + @property + def objects(self): + return self.get_detected_sensor_objects() + + def get_detected_sensor_objects(self, what=None): + for sensor in _collecty.get_detected_sensors(what): + if sensor.type == "temperature": + yield SensorTemperatureObject(self, sensor) + + elif sensor.type == "voltage": + yield SensorVoltageObject(self, sensor) + + elif sensor.type == "fan": + yield SensorFanObject(self, sensor) -- 2.39.2