mirror of
https://github.com/librenms/librenms.git
synced 2024-10-07 16:52:45 +00:00
639 lines
20 KiB
PHP
639 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* Sensor.php
|
|
*
|
|
* Base Sensor class
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
* @package LibreNMS
|
|
* @link http://librenms.org
|
|
* @copyright 2017 Tony Murray
|
|
* @author Tony Murray <murraytony@gmail.com>
|
|
*/
|
|
|
|
namespace LibreNMS\Device;
|
|
|
|
use LibreNMS\Interfaces\Discovery\DiscoveryModule;
|
|
use LibreNMS\Interfaces\Polling\PollerModule;
|
|
use LibreNMS\OS;
|
|
use LibreNMS\RRD\RrdDefinition;
|
|
|
|
class Sensor implements DiscoveryModule, PollerModule
|
|
{
|
|
protected static $name = 'Sensor';
|
|
protected static $table = 'sensors';
|
|
protected static $data_name = 'sensor';
|
|
|
|
private $valid = true;
|
|
|
|
private $sensor_id;
|
|
|
|
private $type;
|
|
private $device_id;
|
|
private $oids;
|
|
private $subtype;
|
|
private $index;
|
|
private $description;
|
|
private $current;
|
|
private $multiplier;
|
|
private $divisor;
|
|
private $aggregator;
|
|
private $high_limit;
|
|
private $low_limit;
|
|
private $high_warn;
|
|
private $low_warn;
|
|
private $entPhysicalIndex;
|
|
private $entPhysicalMeasured;
|
|
|
|
/**
|
|
* Sensor constructor. Create a new sensor to be discovered.
|
|
*
|
|
* @param string $type Class of this sensor, must be a supported class
|
|
* @param int $device_id the device_id of the device that owns this sensor
|
|
* @param array|string $oids an array or single oid that contains the data for this sensor
|
|
* @param string $subtype the type of sensor an additional identifier to separate out sensors of the same class, generally this is the os name
|
|
* @param int|string $index the index of this sensor, must be stable, generally the index of the oid
|
|
* @param string $description A user visible description of this sensor, may be truncated in some places (like graphs)
|
|
* @param int|float $current The current value of this sensor, will seed the db and may be used to guess limits
|
|
* @param int $multiplier a number to multiply the value(s) by
|
|
* @param int $divisor a number to divide the value(s) by
|
|
* @param string $aggregator an operation to combine multiple numbers. Supported: sum, avg
|
|
* @param int|float $high_limit Alerting: Maximum value
|
|
* @param int|float $low_limit Alerting: Minimum value
|
|
* @param int|float $high_warn Alerting: High warning value
|
|
* @param int|float $low_warn Alerting: Low warning value
|
|
* @param int|float $entPhysicalIndex The entPhysicalIndex this sensor is associated, often a port
|
|
* @param int|float $entPhysicalMeasured the table to look for the entPhysicalIndex, for example 'ports' (maybe unused)
|
|
*/
|
|
public function __construct(
|
|
$type,
|
|
$device_id,
|
|
$oids,
|
|
$subtype,
|
|
$index,
|
|
$description,
|
|
$current = null,
|
|
$multiplier = 1,
|
|
$divisor = 1,
|
|
$aggregator = 'sum',
|
|
$high_limit = null,
|
|
$low_limit = null,
|
|
$high_warn = null,
|
|
$low_warn = null,
|
|
$entPhysicalIndex = null,
|
|
$entPhysicalMeasured = null
|
|
) {
|
|
$this->type = $type;
|
|
$this->device_id = $device_id;
|
|
$this->oids = (array)$oids;
|
|
$this->subtype = $subtype;
|
|
$this->index = $index;
|
|
$this->description = $description;
|
|
$this->current = $current;
|
|
$this->multiplier = $multiplier;
|
|
$this->divisor = $divisor;
|
|
$this->aggregator = $aggregator;
|
|
$this->entPhysicalIndex = $entPhysicalIndex;
|
|
$this->entPhysicalMeasured = $entPhysicalMeasured;
|
|
$this->high_limit = $high_limit;
|
|
$this->low_limit = $low_limit;
|
|
$this->high_warn = $high_warn;
|
|
$this->low_warn = $low_warn;
|
|
|
|
// ensure leading dots
|
|
array_walk($this->oids, function (&$oid) {
|
|
$oid = '.' . ltrim($oid, '.');
|
|
});
|
|
|
|
$sensor = $this->toArray();
|
|
// validity not checked yet
|
|
if (is_null($this->current)) {
|
|
$sensor['sensor_oids'] = $this->oids;
|
|
$sensors = array($sensor);
|
|
|
|
$prefetch = self::fetchSnmpData(device_by_id_cache($device_id), $sensors);
|
|
$data = static::processSensorData($sensors, $prefetch);
|
|
|
|
$this->current = current($data);
|
|
$this->valid = is_numeric($this->current);
|
|
}
|
|
|
|
d_echo('Discovered ' . get_called_class() . ' ' . print_r($sensor, true));
|
|
}
|
|
|
|
/**
|
|
* Save this sensor to the database.
|
|
*
|
|
* @return int the sensor_id of this sensor in the database
|
|
*/
|
|
final public function save()
|
|
{
|
|
$db_sensor = $this->fetch();
|
|
|
|
$new_sensor = $this->toArray();
|
|
if ($db_sensor) {
|
|
unset($new_sensor['sensor_current']); // if updating, don't check sensor_current
|
|
$update = array_diff_assoc($new_sensor, $db_sensor);
|
|
|
|
if ($db_sensor['sensor_custom'] == 'Yes') {
|
|
unset($update['sensor_limit']);
|
|
unset($update['sensor_limit_warn']);
|
|
unset($update['sensor_limit_low']);
|
|
unset($update['sensor_limit_low_warn']);
|
|
}
|
|
|
|
if (empty($update)) {
|
|
echo '.';
|
|
} else {
|
|
dbUpdate($this->escapeNull($update), $this->getTable(), '`sensor_id`=?', array($this->sensor_id));
|
|
echo 'U';
|
|
}
|
|
} else {
|
|
$this->sensor_id = dbInsert($this->escapeNull($new_sensor), $this->getTable());
|
|
if ($this->sensor_id !== null) {
|
|
$name = static::$name;
|
|
$message = "$name Discovered: {$this->type} {$this->subtype} {$this->index} {$this->description}";
|
|
log_event($message, $this->device_id, static::$table, 3, $this->sensor_id);
|
|
echo '+';
|
|
}
|
|
}
|
|
|
|
return $this->sensor_id;
|
|
}
|
|
|
|
/**
|
|
* Fetch the sensor from the database.
|
|
* If it doesn't exist, returns null.
|
|
*
|
|
* @return array|null
|
|
*/
|
|
private function fetch()
|
|
{
|
|
$table = $this->getTable();
|
|
if (isset($this->sensor_id)) {
|
|
return dbFetchRow(
|
|
"SELECT `$table` FROM ? WHERE `sensor_id`=?",
|
|
array($this->sensor_id)
|
|
);
|
|
}
|
|
|
|
$sensor = dbFetchRow(
|
|
"SELECT * FROM `$table` " .
|
|
"WHERE `device_id`=? AND `sensor_class`=? AND `sensor_type`=? AND `sensor_index`=?",
|
|
array($this->device_id, $this->type, $this->subtype, $this->index)
|
|
);
|
|
$this->sensor_id = $sensor['sensor_id'];
|
|
return $sensor;
|
|
}
|
|
|
|
/**
|
|
* Get the table for this sensor
|
|
* @return string
|
|
*/
|
|
public function getTable()
|
|
{
|
|
return static::$table;
|
|
}
|
|
|
|
/**
|
|
* Get an array of this sensor with fields that line up with the database.
|
|
* Excludes sensor_id and current
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function toArray()
|
|
{
|
|
return array(
|
|
'sensor_class' => $this->type,
|
|
'device_id' => $this->device_id,
|
|
'sensor_oids' => json_encode($this->oids),
|
|
'sensor_index' => $this->index,
|
|
'sensor_type' => $this->subtype,
|
|
'sensor_descr' => $this->description,
|
|
'sensor_divisor' => $this->divisor,
|
|
'sensor_multiplier' => $this->multiplier,
|
|
'sensor_aggregator' => $this->aggregator,
|
|
'sensor_limit' => $this->high_limit,
|
|
'sensor_limit_warn' => $this->high_warn,
|
|
'sensor_limit_low' => $this->low_limit,
|
|
'sensor_limit_low_warn' => $this->low_warn,
|
|
'sensor_current' => $this->current,
|
|
'entPhysicalIndex' => $this->entPhysicalIndex,
|
|
'entPhysicalIndex_measured' => $this->entPhysicalMeasured,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Escape null values so dbFacile doesn't mess them up
|
|
* honestly, this should be the default, but could break shit
|
|
*
|
|
* @param $array
|
|
* @return array
|
|
*/
|
|
private function escapeNull($array)
|
|
{
|
|
return array_map(function ($value) {
|
|
return is_null($value) ? array('NULL') : $value;
|
|
}, $array);
|
|
}
|
|
|
|
/**
|
|
* Run Sensors discovery for the supplied OS (device)
|
|
*
|
|
* @param OS $os
|
|
*/
|
|
public static function discover(OS $os)
|
|
{
|
|
// Add discovery types here
|
|
}
|
|
|
|
/**
|
|
* Poll sensors for the supplied OS (device)
|
|
*
|
|
* @param OS $os
|
|
*/
|
|
public static function poll(OS $os)
|
|
{
|
|
$table = static::$table;
|
|
|
|
// fetch and group sensors, decode oids
|
|
$sensors = array_reduce(
|
|
dbFetchRows("SELECT * FROM `$table` WHERE `device_id` = ?", array($os->getDeviceId())),
|
|
function ($carry, $sensor) {
|
|
$sensor['sensor_oids'] = json_decode($sensor['sensor_oids']);
|
|
$carry[$sensor['sensor_class']][] = $sensor;
|
|
return $carry;
|
|
},
|
|
array()
|
|
);
|
|
|
|
foreach ($sensors as $type => $type_sensors) {
|
|
// check for custom polling
|
|
$typeInterface = static::getPollingInterface($type);
|
|
if (!interface_exists($typeInterface)) {
|
|
echo "ERROR: Polling Interface doesn't exist! $typeInterface\n";
|
|
}
|
|
|
|
// fetch custom data
|
|
if ($os instanceof $typeInterface) {
|
|
unset($sensors[$type]); // remove from sensors array to prevent double polling
|
|
static::pollSensorType($os, $type, $type_sensors);
|
|
}
|
|
}
|
|
|
|
// pre-fetch all standard sensors
|
|
$standard_sensors = call_user_func_array('array_merge', $sensors);
|
|
$pre_fetch = self::fetchSnmpData($os->getDevice(), $standard_sensors);
|
|
|
|
// poll standard sensors
|
|
foreach ($sensors as $type => $type_sensors) {
|
|
static::pollSensorType($os, $type, $type_sensors, $pre_fetch);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Poll all sensors of a specific class
|
|
*
|
|
* @param OS $os
|
|
* @param string $type
|
|
* @param array $sensors
|
|
* @param array $prefetch
|
|
*/
|
|
protected static function pollSensorType($os, $type, $sensors, $prefetch = array())
|
|
{
|
|
echo "$type:\n";
|
|
|
|
// process data or run custom polling
|
|
$typeInterface = static::getPollingInterface($type);
|
|
if ($os instanceof $typeInterface) {
|
|
d_echo("Using OS polling for $type\n");
|
|
$function = static::getPollingMethod($type);
|
|
$data = $os->$function($sensors);
|
|
} else {
|
|
$data = static::processSensorData($sensors, $prefetch);
|
|
}
|
|
|
|
d_echo($data);
|
|
|
|
self::recordSensorData($os, $sensors, $data);
|
|
}
|
|
|
|
/**
|
|
* Fetch snmp data from the device
|
|
* Return an array keyed by oid
|
|
*
|
|
* @param array $device
|
|
* @param array $sensors
|
|
* @return array
|
|
*/
|
|
private static function fetchSnmpData($device, $sensors)
|
|
{
|
|
$oids = self::getOidsFromSensors($sensors, get_device_oid_limit($device));
|
|
|
|
$snmp_data = array();
|
|
foreach ($oids as $oid_chunk) {
|
|
$multi_data = snmp_get_multi_oid($device, $oid_chunk, '-OUQnt');
|
|
$snmp_data = array_merge($snmp_data, $multi_data);
|
|
}
|
|
|
|
// deal with string values that may be surrounded by quotes
|
|
array_walk($snmp_data, function (&$oid) {
|
|
$oid = trim($oid, '"') + 0;
|
|
});
|
|
|
|
return $snmp_data;
|
|
}
|
|
|
|
|
|
/**
|
|
* Process the snmp data for the specified sensors
|
|
* Returns an array sensor_id => value
|
|
*
|
|
* @param $sensors
|
|
* @param $prefetch
|
|
* @return array
|
|
* @internal param $device
|
|
*/
|
|
protected static function processSensorData($sensors, $prefetch)
|
|
{
|
|
$sensor_data = array();
|
|
foreach ($sensors as $sensor) {
|
|
// pull out the data for this sensor
|
|
$requested_oids = array_flip($sensor['sensor_oids']);
|
|
$data = array_intersect_key($prefetch, $requested_oids);
|
|
|
|
// if no data set null and continue to the next sensor
|
|
if (empty($data)) {
|
|
$data[$sensor['sensor_id']] = null;
|
|
continue;
|
|
}
|
|
|
|
if (count($data) > 1) {
|
|
// aggregate data
|
|
if ($sensor['sensor_aggregator'] == 'avg') {
|
|
$sensor_value = array_sum($data) / count($data);
|
|
} else {
|
|
// sum
|
|
$sensor_value = array_sum($data);
|
|
}
|
|
} else {
|
|
$sensor_value = current($data);
|
|
}
|
|
|
|
if ($sensor['sensor_divisor'] && $sensor_value !== 0) {
|
|
$sensor_value = ($sensor_value / $sensor['sensor_divisor']);
|
|
}
|
|
|
|
if ($sensor['sensor_multiplier']) {
|
|
$sensor_value = ($sensor_value * $sensor['sensor_multiplier']);
|
|
}
|
|
|
|
$sensor_data[$sensor['sensor_id']] = $sensor_value;
|
|
}
|
|
|
|
return $sensor_data;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a list of unique oids from an array of sensors and break it into chunks.
|
|
*
|
|
* @param $sensors
|
|
* @param int $chunk How many oids per chunk. Default 10.
|
|
* @return array
|
|
*/
|
|
private static function getOidsFromSensors($sensors, $chunk = 10)
|
|
{
|
|
// Sort the incoming oids and sensors
|
|
$oids = array_reduce($sensors, function ($carry, $sensor) {
|
|
return array_merge($carry, $sensor['sensor_oids']);
|
|
}, array());
|
|
|
|
// only unique oids and chunk
|
|
$oids = array_chunk(array_keys(array_flip($oids)), $chunk);
|
|
|
|
return $oids;
|
|
}
|
|
|
|
protected static function discoverType(OS $os, $type)
|
|
{
|
|
$typeInterface = static::getDiscoveryInterface($type);
|
|
if (!interface_exists($typeInterface)) {
|
|
echo "ERROR: Discovery Interface doesn't exist! $typeInterface\n";
|
|
}
|
|
|
|
$have_discovery = $os instanceof $typeInterface;
|
|
if ($have_discovery) {
|
|
echo "$type: ";
|
|
$function = static::getDiscoveryMethod($type);
|
|
$sensors = $os->$function();
|
|
if (!is_array($sensors)) {
|
|
c_echo("%RERROR:%n $function did not return an array! Skipping discovery.");
|
|
$sensors = array();
|
|
}
|
|
} else {
|
|
$sensors = array(); // delete non existent sensors
|
|
}
|
|
|
|
self::checkForDuplicateSensors($sensors);
|
|
|
|
self::sync($os->getDeviceId(), $type, $sensors);
|
|
|
|
if ($have_discovery) {
|
|
echo PHP_EOL;
|
|
}
|
|
}
|
|
|
|
private static function checkForDuplicateSensors($sensors)
|
|
{
|
|
$duplicate_check = array();
|
|
$dup = false;
|
|
|
|
foreach ($sensors as $sensor) {
|
|
/** @var Sensor $sensor */
|
|
$key = $sensor->getUniqueId();
|
|
if (isset($duplicate_check[$key])) {
|
|
c_echo("%R ERROR:%n A sensor already exists at this index $key ");
|
|
$dup = true;
|
|
}
|
|
$duplicate_check[$key] = 1;
|
|
}
|
|
|
|
return $dup;
|
|
}
|
|
|
|
/**
|
|
* Returns a string that must be unique for each sensor
|
|
* type (class), subtype (type), index (index)
|
|
*
|
|
* @return string
|
|
*/
|
|
private function getUniqueId()
|
|
{
|
|
return $this->type . '-' . $this->subtype . '-' . $this->index;
|
|
}
|
|
|
|
protected static function getDiscoveryInterface($type)
|
|
{
|
|
return str_to_class($type, 'LibreNMS\\Interfaces\\Discovery\\Sensors\\') . 'Discovery';
|
|
}
|
|
|
|
protected static function getDiscoveryMethod($type)
|
|
{
|
|
return 'discover' . str_to_class($type);
|
|
}
|
|
|
|
protected static function getPollingInterface($type)
|
|
{
|
|
return str_to_class($type, 'LibreNMS\\Interfaces\\Polling\\Sensors\\') . 'Polling';
|
|
}
|
|
|
|
protected static function getPollingMethod($type)
|
|
{
|
|
return 'poll' . str_to_class($type);
|
|
}
|
|
|
|
/**
|
|
* Is this sensor valid?
|
|
* If not, it should not be added to or in the database
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isValid()
|
|
{
|
|
return $this->valid;
|
|
}
|
|
|
|
/**
|
|
* Save sensors and remove invalid sensors
|
|
* This the sensors array should contain all the sensors of a specific class
|
|
* It may contain sensors from multiple tables and devices, but that isn't the primary use
|
|
*
|
|
* @param int $device_id
|
|
* @param string $type
|
|
* @param array $sensors
|
|
*/
|
|
final public static function sync($device_id, $type, array $sensors)
|
|
{
|
|
// save and collect valid ids
|
|
$valid_sensor_ids = array();
|
|
foreach ($sensors as $sensor) {
|
|
/** @var $this $sensor */
|
|
if ($sensor->isValid()) {
|
|
$valid_sensor_ids[] = $sensor->save();
|
|
}
|
|
}
|
|
|
|
// delete invalid sensors
|
|
self::clean($device_id, $type, $valid_sensor_ids);
|
|
}
|
|
|
|
/**
|
|
* Remove invalid sensors. Passing an empty array will remove all sensors of that class
|
|
*
|
|
* @param int $device_id
|
|
* @param string $type
|
|
* @param array $sensor_ids valid sensor ids
|
|
*/
|
|
private static function clean($device_id, $type, $sensor_ids)
|
|
{
|
|
$table = static::$table;
|
|
$params = array($device_id, $type);
|
|
$where = '`device_id`=? AND `sensor_class`=?';
|
|
|
|
if (!empty($sensor_ids)) {
|
|
$where .= ' AND `sensor_id` NOT IN ' . dbGenPlaceholders(count($sensor_ids));
|
|
$params = array_merge($params, $sensor_ids);
|
|
}
|
|
|
|
$delete = dbFetchRows("SELECT * FROM `$table` WHERE $where", $params);
|
|
foreach ($delete as $sensor) {
|
|
echo '-';
|
|
|
|
$message = static::$name;
|
|
$message .= " Deleted: $type {$sensor['sensor_type']} {$sensor['sensor_index']} {$sensor['sensor_descr']}";
|
|
log_event($message, $device_id, static::$table, 3, $sensor['sensor_id']);
|
|
}
|
|
if (!empty($delete)) {
|
|
dbDelete($table, $where, $params);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a list of valid types with metadata about each type
|
|
* $class => array(
|
|
* 'short' - short text for this class
|
|
* 'long' - long text for this class
|
|
* 'unit' - units used by this class 'dBm' for example
|
|
* 'icon' - font awesome icon used by this class
|
|
* )
|
|
* @param bool $valid filter this list by valid types in the database
|
|
* @param int $device_id when filtering, only return types valid for this device_id
|
|
* @return array
|
|
*/
|
|
public static function getTypes($valid = false, $device_id = null)
|
|
{
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Record sensor data in the database and data stores
|
|
*
|
|
* @param $os
|
|
* @param $sensors
|
|
* @param $data
|
|
*/
|
|
protected static function recordSensorData(OS $os, $sensors, $data)
|
|
{
|
|
$types = static::getTypes();
|
|
|
|
foreach ($sensors as $sensor) {
|
|
$sensor_value = $data[$sensor['sensor_id']];
|
|
|
|
echo " {$sensor['sensor_descr']}: $sensor_value {$types[$sensor['sensor_class']]['unit']}\n";
|
|
|
|
// update rrd and database
|
|
$rrd_name = array(
|
|
static::$data_name,
|
|
$sensor['sensor_class'],
|
|
$sensor['sensor_type'],
|
|
$sensor['sensor_index']
|
|
);
|
|
$rrd_def = RrdDefinition::make()->addDataset('sensor', 'GAUGE');
|
|
|
|
$fields = array(
|
|
'sensor' => isset($sensor_value) ? $sensor_value : 'U',
|
|
);
|
|
|
|
$tags = array(
|
|
'sensor_class' => $sensor['sensor_class'],
|
|
'sensor_type' => $sensor['sensor_type'],
|
|
'sensor_descr' => $sensor['sensor_descr'],
|
|
'sensor_index' => $sensor['sensor_index'],
|
|
'rrd_name' => $rrd_name,
|
|
'rrd_def' => $rrd_def
|
|
);
|
|
data_update($os->getDevice(), static::$data_name, $tags, $fields);
|
|
|
|
$update = array(
|
|
'sensor_prev' => $sensor['sensor_current'],
|
|
'sensor_current' => $sensor_value,
|
|
'lastupdate' => array('NOW()'),
|
|
);
|
|
dbUpdate($update, static::$table, "`sensor_id` = ?", array($sensor['sensor_id']));
|
|
}
|
|
}
|
|
}
|