. * * @package LibreNMS * @link http://librenms.org * @copyright 2017 Tony Murray * @author Tony Murray */ 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'])); } } }