LibreNMS/LibreNMS/Device/Sensor.php

650 lines
21 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\Config;
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 runDiscovery(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;
$query = "SELECT * FROM `$table` WHERE `device_id` = ?";
$params = [$os->getDeviceId()];
$submodules = Config::get('poller_submodules.wireless', []);
if (!empty($submodules)) {
$query .= " AND `sensor_class` IN " . dbGenPlaceholders(count($submodules));
$params = array_merge($params, $submodules);
}
// fetch and group sensors, decode oids
$sensors = array_reduce(
dbFetchRows($query, $params),
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 = collect($sensors)->flatten(1)->all();
$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_type = isset($types[$sensor['sensor_class']]['type']) ? strtoupper($types[$sensor['sensor_class']]['type']) : 'GAUGE';
$rrd_def = RrdDefinition::make()->addDataset('sensor', $rrd_type);
$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']));
}
}
}