Files
librenms-librenms/LibreNMS/Device/Sensor.php
Tony Murray 11147d3bbf Major Processors rewrite (#8066)
* Extract DiscoveryItem and move some things to better places.
Extract model class
Fix up model construction.  I have problem with construction...
Makeshift model working.  Switch constructor to factory.  discover() and create()
Support legacy discovery.
Remove uneeded custom pollers
Remove netonix custom detection as we try ucd on all os now.
Add a few yaml procs.  Fix a couple things.
More processor discovery conversions
Move Calix e7 to standard hrProcessorLoad, but it doesn't fully implement the HR-MIB, move things around to make it work.
Add a few yaml procs.  Fix a couple things. Correct some stupid mib stuff.
Move more, drop php 5.3
Add netscaler which uses string indexes.  Port fiberhome to yaml and use skip_values
More conversions.  BroadcomProcessorUsage Trait
Serveriron and Ironware share some mibs.  Create a common abstract os for them.
Add yaml support for mib specification in each data entry
Make legacy discover_processor() set 0 for hrDeviceIndex

Untangle Dell switch OS processors

Use use shared OS for groups if they don't have a specific group.
fix silly mib mistake

Make index optional

Move HR and UCD to Traits and out of Processor.

* forgot to update the fortiswitch index

* Make sgos and avaya-ers match the old index.

* fix comware test data

* fix merge errors

* fix dsm and remove pointless empty modules

* file not found exception is in the wrong place.

* Updated processor development docs
2018-02-05 07:39:13 -06:00

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 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;
// 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']));
}
}
}