librenms/includes/dbFacile.php
Tony Murray 4c0412b14d feature: Wireless Sensors Overhaul (#6471)
* feature: Wireless Sensors
Includes client counts for ios and unifi
Graphing could use some improvement.
Alerting and threshold ui not implemented

WIP: starting OO based wireless sensors.

Class based functionality working
remove old functional files
add schema file

discovery needs to be enabled, not polling

fix up schema

fix Unifi discovery not returning an array

Add some debug when discovering a sensor.
Fix style.

Add missing semicolin

Add a null object (Generic) for OS.
Fill out some phpdocs

Re-organized code
Each sensor type now has it's own discovery and polling interface
Custom polling tested with Unifi CCQ

Left to do:
Implement UI (Graphs and Custom thresholds)
Alerting
Testing

Fix event message text

Remove runDiscovery and runPolling from OS, they are unused and don't belong there.

Cleanups/docs

Missed this file.

Remove the requirement to fetch the current value to check validity.
Do that automatically if current is not specified
A few cleanups here and there

First pass at graphing.
device_ and wireless_ graphs added.

Add RouterOS support

Singleton OS instance isn't required right now.
Remove that to allow some memory to be freed.

Add wireless to the device list metrics.
Make all metrics clickable

Tweak graphs a bit

Implement limit configuration page.
Use sensors page as common code instead of duplicating.
Clean up some javascript interactions:  Allow enter on values to save. Cancel if update is not needed. Enable the clear custom button after setting a custom value.
Add some wireless alert rules to the library.

Add documentation.

Add unifi client counts by ssid in addition to radio.
Optimize Sensor polling a bit.

Add HP MSM clients support (for full controller)
Fix function accessibility

Formalize the discovery and poller interfaces.

Add Xirrus clients and noise floor
move module interfaces to a more appropriate place.
push caching code up to os, unsure about this do to the limitations

No point in selectively enabling wireless discovery.  We only discover if the device supports something.

Add RSSI, Power, and Rate.
Add these sensors for Ubnt Airos.
Clean up some copyrights.

Reduce the amount of files need to add new types.
Leave graph files for consistency and to allow customization.

Remove the old wifi clients graph completely.
ciscowlc should have improved counts (total and per-ssid)

Schema didn't get added.

Impelement the rest of the AirOS sensors
Reformat and re-organize the Airos.php class.

Add several UBNT AirFiber sensors

A few fixes add links to the section headers

Add HP MSM mibs.

* Schema file got dropped in rebase.

* Add wireless menu to view sensors across all devices.
Icons in the menu need help :/

* Add HeliOS, Mimosa, and Siklu support
Sensors added SNR + Noise

* Add power and utilization to Unifi

* Update polling to prefetch all sensor data in a few snmp requests as possible

* Add Extendair: tx+rx power, aggregate rate, frequency

* Add a check for duplicate sensors in discovery.  Just print an error for now.

* Add Bit Error Ratio (named error-ratio to allow for bit error rate to be added if needed)
Fix an incorrect link in the wireless sensors table

* Add error rate and change all bps and Hz to use si units

* Fixes to limits and frequency display

* Fix overview graph frequency display
A few decimal place tweaks

* Don't allow switching sensor and wireless-sensor graphs, it doesn't work.
Change individual distance graphs to use si units

* Go through the OS and make sure I got all the sensors I can (probably missed some still)
Because pollWirelessChannelAsFrequency() is generic and a little complex, so pull it up to OS.
Message to help developers adding supports that don't return an array from discover functions.

* Fix some issues

* Remove noise and signal for now at least
A couple more fixes
Add a notification

* Oopsie

* Bonus AirFiber sensors
2017-05-01 23:49:11 -05:00

575 lines
16 KiB
PHP

<?php
/*
* dbFacile - A Database API that should have existed from the start
* Version 0.4.3
*
* This code is covered by the MIT license http://en.wikipedia.org/wiki/MIT_License
*
* By Alan Szlosek from http://www.greaterscope.net/projects/dbFacile
*
* The non-OO version of dbFacile. It's a bit simplistic, but gives you the
* really useful bits in non-class form.
*
* Usage
* 1. Connect to MySQL as you normally would ... this code uses an existing connection
* 2. Use dbFacile as you normally would, without the object context
* 3. Oh, and dbFetchAll() is now dbFetchRows()
*/
use LibreNMS\Exceptions\DatabaseConnectException;
/**
* Connect to the database.
* Will use global $config variables if they are not sent: db_host, db_user, db_pass, db_name, db_port, db_socket
*
* @param string $host
* @param string $user
* @param string $password
* @param string $database
* @param string $port
* @param string $socket
* @return mysqli
* @throws DatabaseConnectException
*/
function dbConnect($host = null, $user = '', $password = '', $database = '', $port = null, $socket = null)
{
global $config, $database_link;
$host = empty($host) ? $config['db_host'] : $host;
$user = empty($user) ? $config['db_user'] : $user;
$password = empty($password) ? $config['db_pass'] : $password;
$database = empty($database) ? $config['db_name'] : $database;
$port = empty($port) ? $config['db_port'] : $port;
$socket = empty($socket) ? $config['db_socket'] : $socket;
$database_link = mysqli_connect('p:' . $host, $user, $password, null, $port, $socket);
if ($database_link === false) {
$error = mysqli_connect_error();
if ($error == 'No such file or directory') {
$error = 'Could not connect to ' . $host;
}
throw new DatabaseConnectException($error);
}
$database_db = mysqli_select_db($database_link, $config['db_name']);
if (!$database_db) {
$db_create_sql = "CREATE DATABASE " . $config['db_name'] . " CHARACTER SET utf8 COLLATE utf8_unicode_ci";
mysqli_query($database_link, $db_create_sql);
$database_db = mysqli_select_db($database_link, $config['db_name']);
}
if (!$database_db) {
throw new DatabaseConnectException("Could not select database: $database. " . mysqli_error($database_link));
}
dbQuery("SET NAMES 'utf8'");
dbQuery("SET CHARACTER SET 'utf8'");
dbQuery("SET COLLATION_CONNECTION = 'utf8_unicode_ci'");
return $database_link;
}
/*
* Performs a query using the given string.
* Used by the other _query functions.
* */
function dbQuery($sql, $parameters = array())
{
global $fullSql, $debug, $sql_debug, $database_link, $config;
$fullSql = dbMakeQuery($sql, $parameters);
if ($debug) {
if (php_sapi_name() == 'cli' && empty($_SERVER['REMOTE_ADDR'])) {
if (preg_match('/(INSERT INTO `alert_log`).*(details)/i', $fullSql)) {
echo "\nINSERT INTO `alert_log` entry masked due to binary data\n";
} else {
c_echo('SQL[%y'.$fullSql."%n] \n");
}
} else {
$sql_debug[] = $fullSql;
}
}
$result = mysqli_query($database_link, $fullSql);
if (!$result) {
$mysql_error = mysqli_error($database_link);
if ((in_array($config['mysql_log_level'], array('INFO', 'ERROR')) && !preg_match('/Duplicate entry/', $mysql_error)) || (in_array($config['mysql_log_level'], array('DEBUG')))) {
if (!empty($mysql_error)) {
logfile(date($config['dateformat']['compact']) . " MySQL Error: $mysql_error ($fullSql)");
}
}
}
return $result;
}//end dbQuery()
/*
* Passed an array and a table name, it attempts to insert the data into the table.
* Check for boolean false to determine whether insert failed
* */
function dbInsert($data, $table)
{
global $database_link;
$time_start = microtime(true);
// the following block swaps the parameters if they were given in the wrong order.
// it allows the method to work for those that would rather it (or expect it to)
// follow closer with SQL convention:
// insert into the TABLE this DATA
if (is_string($data) && is_array($table)) {
$tmp = $data;
$data = $table;
$table = $tmp;
// trigger_error('QDB - Parameters passed to insert() were in reverse order, but it has been allowed', E_USER_NOTICE);
}
$sql = 'INSERT INTO `'.$table.'` (`'.implode('`,`', array_keys($data)).'`) VALUES ('.implode(',', dbPlaceHolders($data)).')';
dbBeginTransaction();
$result = dbQuery($sql, $data);
if ($result) {
$id = mysqli_insert_id($database_link);
dbCommitTransaction();
// return $id;
} else {
if ($table != 'Contact') {
trigger_error('QDB - Insert failed.', E_USER_WARNING);
}
dbRollbackTransaction();
$id = null;
}
recordDbStatistic('insert', $time_start);
return $id;
}//end dbInsert()
/*
* Passed an array and a table name, it attempts to insert the data into the table.
* $data is an array (rows) of key value pairs. keys are fields. Rows need to have same fields.
* Check for boolean false to determine whether insert failed
* */
function dbBulkInsert($data, $table)
{
$time_start = microtime(true);
// the following block swaps the parameters if they were given in the wrong order.
// it allows the method to work for those that would rather it (or expect it to)
// follow closer with SQL convention:
// insert into the TABLE this DATA
if (is_string($data) && is_array($table)) {
$tmp = $data;
$data = $table;
$table = $tmp;
}
if (count($data) === 0) {
return false;
}
if (count($data[0]) === 0) {
return false;
}
$sql = 'INSERT INTO `'.$table.'` (`'.implode('`,`', array_keys($data[0])).'`) VALUES ';
$values ='';
foreach ($data as $row) {
if ($values != '') {
$values .= ',';
}
$rowvalues='';
foreach ($row as $key => $value) {
if ($rowvalues != '') {
$rowvalues .= ',';
}
$rowvalues .= "'".mres($value)."'";
}
$values .= "(".$rowvalues.")";
}
$result = dbQuery($sql.$values);
recordDbStatistic('insert', $time_start);
return $result;
}//end dbBulkInsert()
/*
* Passed an array, table name, WHERE clause, and placeholder parameters, it attempts to update a record.
* Returns the number of affected rows
* */
function dbUpdate($data, $table, $where = null, $parameters = array())
{
global $fullSql, $database_link;
$time_start = microtime(true);
// the following block swaps the parameters if they were given in the wrong order.
// it allows the method to work for those that would rather it (or expect it to)
// follow closer with SQL convention:
// update the TABLE with this DATA
if (is_string($data) && is_array($table)) {
$tmp = $data;
$data = $table;
$table = $tmp;
// trigger_error('QDB - The first two parameters passed to update() were in reverse order, but it has been allowed', E_USER_NOTICE);
}
// need field name and placeholder value
// but how merge these field placeholders with actual $parameters array for the WHERE clause
$sql = 'UPDATE `'.$table.'` set ';
foreach ($data as $key => $value) {
$sql .= '`'.$key.'` '.'=:'.$key.',';
}
$sql = substr($sql, 0, -1);
// strip off last comma
if ($where) {
$sql .= ' WHERE '.$where;
$data = array_merge($data, $parameters);
}
if (dbQuery($sql, $data)) {
$return = mysqli_affected_rows($database_link);
} else {
// echo("$fullSql");
trigger_error('QDB - Update failed.', E_USER_WARNING);
$return = false;
}
recordDbStatistic('update', $time_start);
return $return;
}//end dbUpdate()
function dbDelete($table, $where = null, $parameters = array())
{
global $database_link;
$time_start = microtime(true);
$sql = 'DELETE FROM `'.$table.'`';
if ($where) {
$sql .= ' WHERE '.$where;
}
$result = dbQuery($sql, $parameters);
recordDbStatistic('delete', $time_start);
if ($result) {
return mysqli_affected_rows($database_link);
} else {
return false;
}
}//end dbDelete()
/*
* Fetches all of the rows (associatively) from the last performed query.
* Most other retrieval functions build off this
* */
function dbFetchRows($sql, $parameters = array(), $nocache = false)
{
global $config;
if ($config['memcached']['enable'] && $nocache === false) {
$result = $config['memcached']['resource']->get(hash('sha512', $sql.'|'.serialize($parameters)));
if (!empty($result)) {
return $result;
}
}
$time_start = microtime(true);
$result = dbQuery($sql, $parameters);
if (mysqli_num_rows($result) > 0) {
$rows = array();
while ($row = mysqli_fetch_assoc($result)) {
$rows[] = $row;
}
mysqli_free_result($result);
if ($config['memcached']['enable'] && $nocache === false) {
$config['memcached']['resource']->set(hash('sha512', $sql.'|'.serialize($parameters)), $rows, $config['memcached']['ttl']);
}
recordDbStatistic('fetchrows', $time_start);
return $rows;
}
mysqli_free_result($result);
// no records, thus return empty array
// which should evaluate to false, and will prevent foreach notices/warnings
recordDbStatistic('fetchrows', $time_start);
return array();
}//end dbFetchRows()
/*
* This is intended to be the method used for large result sets.
* It is intended to return an iterator, and act upon buffered data.
* */
function dbFetch($sql, $parameters = array(), $nocache = false)
{
return dbFetchRows($sql, $parameters, $nocache);
/*
// for now, don't do the iterator thing
$result = dbQuery($sql, $parameters);
if($result) {
// return new iterator
return new dbIterator($result);
} else {
return null; // ??
}
*/
}//end dbFetch()
/*
* Like fetch(), accepts any number of arguments
* The first argument is an sprintf-ready query stringTypes
* */
function dbFetchRow($sql = null, $parameters = array(), $nocache = false)
{
global $config;
if ($config['memcached']['enable'] && $nocache === false) {
$result = $config['memcached']['resource']->get(hash('sha512', $sql.'|'.serialize($parameters)));
if (!empty($result)) {
return $result;
}
}
$time_start = microtime(true);
$result = dbQuery($sql, $parameters);
if ($result) {
$row = mysqli_fetch_assoc($result);
mysqli_free_result($result);
recordDbStatistic('fetchrow', $time_start);
if ($config['memcached']['enable'] && $nocache === false) {
$config['memcached']['resource']->set(hash('sha512', $sql.'|'.serialize($parameters)), $row, $config['memcached']['ttl']);
}
return $row;
} else {
return null;
}
}//end dbFetchRow()
/*
* Fetches the first call from the first row returned by the query
* */
function dbFetchCell($sql, $parameters = array(), $nocache = false)
{
$time_start = microtime(true);
$row = dbFetchRow($sql, $parameters, $nocache);
recordDbStatistic('fetchcell', $time_start);
if ($row) {
return array_shift($row);
// shift first field off first row
}
return null;
}//end dbFetchCell()
/*
* This method is quite different from fetchCell(), actually
* It fetches one cell from each row and places all the values in 1 array
* */
function dbFetchColumn($sql, $parameters = array(), $nocache = false)
{
$time_start = microtime(true);
$cells = array();
foreach (dbFetch($sql, $parameters, $nocache) as $row) {
$cells[] = array_shift($row);
}
recordDbStatistic('fetchcolumn', $time_start);
return $cells;
}//end dbFetchColumn()
/*
* Should be passed a query that fetches two fields
* The first will become the array key
* The second the key's value
*/
function dbFetchKeyValue($sql, $parameters = array(), $nocache = false)
{
$data = array();
foreach (dbFetch($sql, $parameters, $nocache) as $row) {
$key = array_shift($row);
if (sizeof($row) == 1) {
// if there were only 2 fields in the result
// use the second for the value
$data[$key] = array_shift($row);
} else {
// if more than 2 fields were fetched
// use the array of the rest as the value
$data[$key] = $row;
}
}
return $data;
}//end dbFetchKeyValue()
/*
* This combines a query and parameter array into a final query string for execution
* PDO drivers don't need to use this
*/
function dbMakeQuery($sql, $parameters)
{
// bypass extra logic if we have no parameters
if (sizeof($parameters) == 0) {
return $sql;
}
$parameters = dbPrepareData($parameters);
// separate the two types of parameters for easier handling
$questionParams = array();
$namedParams = array();
foreach ($parameters as $key => $value) {
if (is_numeric($key)) {
$questionParams[] = $value;
} else {
$namedParams[':'.$key] = $value;
}
}
// sort namedParams in reverse to stop substring squashing
krsort($namedParams);
// split on question-mark and named placeholders
if (preg_match('/(\[\[:[\w]+:\]\])/', $sql)) {
$result = preg_split('/(\?[a-zA-Z0-9_-]*)/', $sql, -1, (PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE));
} else {
$result = preg_split('/(\?|:[a-zA-Z0-9_-]+)/', $sql, -1, (PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE));
}
// every-other item in $result will be the placeholder that was found
$query = '';
$res_size = sizeof($result);
for ($i = 0; $i < $res_size; $i += 2) {
$query .= $result[$i];
$j = ($i + 1);
if (array_key_exists($j, $result)) {
$test = $result[$j];
if ($test == '?') {
$query .= array_shift($questionParams);
} else {
$query .= $namedParams[$test];
}
}
}
return $query;
}//end dbMakeQuery()
function dbPrepareData($data)
{
global $database_link;
$values = array();
foreach ($data as $key => $value) {
$escape = true;
// don't quote or esc if value is an array, we treat it
// as a "decorator" that tells us not to escape the
// value contained in the array
if (is_array($value) && !is_object($value)) {
$escape = false;
$value = array_shift($value);
}
// it's not right to worry about invalid fields in this method because we may be operating on fields
// that are aliases, or part of other tables through joins
// if(!in_array($key, $columns)) // skip invalid fields
// continue;
if ($escape) {
$values[$key] = "'".mysqli_real_escape_string($database_link, $value)."'";
} else {
$values[$key] = $value;
}
}
return $values;
}//end dbPrepareData()
/**
* Given a data array, this returns an array of placeholders
* These may be question marks, or ":email" type
*
* @param array $values
* @return array
*/
function dbPlaceHolders($values)
{
$data = array();
foreach ($values as $key => $value) {
if (is_numeric($key)) {
$data[] = '?';
} else {
$data[] = ':'.$key;
}
}
return $data;
}//end dbPlaceHolders()
function dbBeginTransaction()
{
global $database_link;
mysqli_query($database_link, 'begin');
}//end dbBeginTransaction()
function dbCommitTransaction()
{
global $database_link;
mysqli_query($database_link, 'commit');
}//end dbCommitTransaction()
function dbRollbackTransaction()
{
global $database_link;
mysqli_query($database_link, 'rollback');
}//end dbRollbackTransaction()
/**
* Generate a string of placeholders to pass to fill in a list
* result will look like this: (?, ?, ?, ?)
*
* @param $count
* @return string placholder list
*/
function dbGenPlaceholders($count)
{
return '(' . implode(',', array_fill(0, $count, '?')) . ')';
}