From c63b7119cbb78d4c841b6a553c2309c3622cceed Mon Sep 17 00:00:00 2001 From: Aaron Daniels Date: Sat, 15 Aug 2015 16:01:43 +1000 Subject: [PATCH 1/9] Device Components. The purpose of this feature is to provide a framework for discovery modules to store information in the database, without needing to add new tables for each feature. This Feature provides: - A database structure to store data. - An API to store and retrieve data from the database. - Integration to the LibreNMS APIv0. - Ability to disable/ignore components. - Default alerting rules. - The API returns $COMPONENT[$device_id][$component_id] to allow pulling of data for multiple devices. The intent is to be able to create 'Applications' the consolidate data from applications covering multiple devices. - Added some developer documentation --- doc/API/API-Docs.md | 65 +++++ doc/Extensions/Component.md | 309 +++++++++++++++++++++++ html/api_v0.php | 2 + html/includes/api_functions.inc.php | 54 ++++ html/includes/forms/component.inc.php | 84 ++++++ html/includes/functions.inc.php | 10 + html/includes/table/component.inc.php | 59 +++++ html/pages/device/edit.inc.php | 2 + html/pages/device/edit/component.inc.php | 102 ++++++++ includes/component.php | 254 +++++++++++++++++++ sql-schema/084.sql | 6 + 11 files changed, 947 insertions(+) create mode 100644 doc/Extensions/Component.md create mode 100644 html/includes/forms/component.inc.php create mode 100644 html/includes/table/component.inc.php create mode 100644 html/pages/device/edit/component.inc.php create mode 100644 includes/component.php create mode 100644 sql-schema/084.sql diff --git a/doc/API/API-Docs.md b/doc/API/API-Docs.md index e879fe637..6b6e0afbb 100644 --- a/doc/API/API-Docs.md +++ b/doc/API/API-Docs.md @@ -13,6 +13,7 @@ - [`get_graphs`](#api-route-5) - [`get_graph_generic_by_hostname`](#api-route-6) - [`get_port_graphs`](#api-route-7) + - [`get_components`](#api-route-25) - [`get_port_stats_by_port_hostname`](#api-route-8) - [`get_graph_by_port_hostname`](#api-route-9) - [`list_devices`](#api-route-10) @@ -267,6 +268,70 @@ Output: } ``` +### Function: `get_components` [`top`](#top) + +Get a list of components for a particular device. + +Route: /api/v0/devices/:hostname/components + +- hostname can be either the device hostname or id + +Input: + + - type: Filter the result by type (Equals). + - id: Filter the result by id (Equals). + - label: Filter the result by label (Contains). + - status: Filter the result by status (Equals). + - disabled: Filter the result by disabled (Equals). + - ignore: Filter the result by ignore (Equals). + +Example: +```curl +curl -H 'X-Auth-Token: YOURAPITOKENHERE' https://librenms.org/api/v0/devices/localhost/components +``` + +Output: + +```text +{ + "status": "ok", + "err-msg": "", + "count": 3, + "components": { + "2": { + "TestAttribute-1": "Value1", + "TestAttribute-2": "Value2", + "TestAttribute-3": "Value3", + "type": "TestComponent-1", + "label": "This is a really cool blue component", + "status": "1", + "ignore": "0", + "disabled": "0" + }, + "20": { + "TestAttribute-1": "Value4", + "TestAttribute-2": "Value5", + "TestAttribute-3": "Value6", + "type": "TestComponent-1", + "label": "This is a really cool red component", + "status": "1", + "ignore": "0", + "disabled": "0" + }, + "27": { + "TestAttribute-1": "Value7", + "TestAttribute-2": "Value8", + "TestAttribute-3": "Value9", + "type": "TestComponent-2", + "label": "This is a really cool yellow widget", + "status": "1", + "ignore": "0", + "disabled": "0" + } + } +} +``` + ### Function: `get_port_stats_by_port_hostname` [`top`](#top) Get information about a particular port for a device. diff --git a/doc/Extensions/Component.md b/doc/Extensions/Component.md new file mode 100644 index 000000000..2837db899 --- /dev/null +++ b/doc/Extensions/Component.md @@ -0,0 +1,309 @@ +Table of Content: + +- [About](#about) +- [Database Structure](#database) + - [Reserved Fields](#reserved) +- [Using Components](#using) + - [Getting Current Components](#get) + - [Options](#options) + - [Filtering Results](#filter) + - [Sorting Results](#sort) + - [Creating Components](#create) + - [Deleting Components](#delete) + - [Editing Components](#update) + - [Edit the Array](#update-edit) + - [Writing the Components](#update-write) + - [API](#api) + - [Alerting](#alert) +- [Example Code](#example) + + +# About + +The Component extension provides a generic database storage mechanism for discovery and poller modules. +The Driver behind this extension was to provide the features of ports, in a generic manner to discovery/poller modules. + +It provides a status (Normal or Alert), the ability to Disable (do not poll), or Ignore (do not Alert). + +# Database Structure + +The database structure contains the component table: + +```SQL +mysql> select * from component limit 1; ++----+-----------+------+------------+--------+----------+--------+-------+ +| id | device_id | type | label | status | disabled | ignore | error | ++----+-----------+------+------------+--------+----------+--------+-------+ +| 9 | 1 | TEST | TEST LABEL | 0 | 1 | 1 | | ++----+-----------+------+------------+--------+----------+--------+-------+ +1 row in set (0.00 sec) +``` + +These fields are described below: + +- id - ID for each component, unique index +- device_id - device_id from the devices table +- type - name from the component_type table +- label - Display label for the component +- status - The status of the component, retrieved from the device +- disabled - Should this component be polled? +- ignore - Should this component be alerted on +- error - Error message if in Alert state + +The component_prefs table holds custom data in an Attribute/Value format: + +```SQL +mysql> select * from component_prefs limit 1; ++----+-----------+-----------+-----------+ +| id | component | attribute | value | ++----+-----------+-----------+-----------+ +| 4 | 9 | TEST_ATTR | TEST_ATTR | ++----+-----------+-----------+-----------+ +2 rows in set (0.00 sec) +``` + +## Reserved Fields + +When this data from both the component and component_prefs tables is returned in one single consolidated array, there is the potential for someone to attempt to set an attribute (in the component_prefs) table that is used in the components table. +Because of this all fields of the component table are reserved, they cannot be used as custom attributes, if you update these the module will attempt to write them to the component table, not the component_prefs table. + + +# Using Components + +To use components in you application, first you need to include the code. + +From a Discovery/Poller module: + +```php +require_once 'includes/component.php'; +``` + +From the html tree: + +```php +require_once "../includes/component.php"; +``` + +Once the code is loaded, create an instance of the component class: + +```php +$COMPONENT = new component(); +``` + +## Retrieving Components + +Now you can retrieve an array of the available components: + +```php +$ARRAY = $COMPONENT->getComponents($DEVICE_ID,$OPTIONS); +``` + +`getComponents` takes 2 arguments: + +- DEVICE_ID or null for all devices. +- OPTIONS - an array of various options. + +`getComponents` will return an array containing components in the following format: + + Array + ( + [X] => Array + ( + [Y1] => Array + ( + [device_id] => 1 + [TEST_ATTR] => TEST_ATTR + [type] => TEST + [label] => TEST LABEL + [status] => 0 + [ignore] => 1 + [disabled] => 1 + [error] => + ), + [Y2] => Array + ( + [device_id] => 1 + [TEST_ATTR] => TEST_ATTR + [type] => TESTING + [label] => TEST LABEL + [status] => 0 + [ignore] => 1 + [disabled] => 0 + [error] => + ), + ) + ) +Where X is the Device ID and Y1/Y2 is the Component ID. In the example above, 'TEST_ATTR' is a custom field, the rest are reserved fields. + +### Options + +Options can be supplied to `getComponents` to influence which and how components are returned. + +#### Filtering + +You can filter on any of the [reserved](#reserved) fields. Filters are created in the following format: + +```php +$OPTIONS['filter'][FIELD] = array ('OPERATOR', 'CRITERIA'); +``` + +Where: + +- FIELD - The [reserved](#reserved) field to filter on +- OPERATOR - 'LIKE' or '=', are we checking if the FIELD equals or contains the CRITERIA. +- CRITERIA - The criteria to search on + + +There are 2 filtering shortcuts: + +$DEVICE_ID is a synonym for: + +```php +$OPTIONS['filter']['device_id'] = array ('=', $DEVICE_ID); +``` + +`$OPTIONS['type'] = $TYPE` is a synonym for: + +```php +$OPTIONS['filter']['type'] = array ('=', $TYPE); +``` + +#### Sorting + +You can sort the records that are returned by specifying the following option: + +```php +$OPTIONS['sort'][FIELD] = 'DIRECTION'; +``` + +Where Direction is one of: + +- ASC - Ascending, from Low to High +- DESC - Descending, from High to Low + + +## Creating Components + +To create a new component, run the createComponent function. + +```php +$ARRAY = $COMPONENT->createComponent($DEVICE_ID,$TYPE); +``` +`createComponent` takes 2 arguments: + +- DEVICE_ID - The ID of the device to attach the component to. +- TYPE - The unique type for your module. + +This will return a new, empty array with a component ID and Type set, all other fields will be set to defaults. + + Array + ( + [1] => Array + ( + [type] => TESTING + [label] => + [status] => 1 + [ignore] => 0 + [disabled] => 0 + [error] => + ) + ) + + +## Deleting Components + +When a component is no longer needed, it can be deleted. + +```php +$COMPONENT->deleteComponent($COMPONENT_ID) +``` + +This will return True on success or False on failure. + + +## Editing Components + +To edit a component, the procedure is: + +1. [Get the Current Components](#get) +2. [Edit the array](#update-edit) +3. [Write the components](#update-write) + +### Edit the Array + +Once you have a component array from getComponents the first thing to do is extract the components for only the single device you are editing. This is required because the setComponentPrefs function only saves a single device at a time. + +```php +$ARRAY = $COMPONENT->getComponents($DEVICE_ID,$OPTIONS); +$ARRAY = $ARRAY[$DEVICE_ID]; +``` + +Then simply edit this array to suit your needs. +If you need to add a new Attribute/Value pair you can: + +```php +$ARRAY[COMPONENT_ID]['New Attribute'] = 'Value'; +``` + +If you need to delete a previously set Attribute/Value pair you can: + +```php +unset($ARRAY[COMPONENT_ID]['New Attribute']); +``` + +If you need to edit a previously set Attribute/Value pair you can: + +```php +$ARRAY[COMPONENT_ID]['Existing Attribute'] = "New Value"; +``` + + +### Write the components + +To write component changes back to the database simply: + +```php +$COMPONENT->setComponentPrefs($DEVICE_ID,$ARRAY) +``` + +When writing the component array there are several caveats to be aware of, these are: + +- $ARRAY must be in the format of a single device ID - `$ARRAY[$COMPONENT_ID][Attribute] = 'Value';` NOT in the multi device format returned by getComponents - $ARRAY[$DEVICE_ID][$COMPONENT_ID][Attribute] = 'Value';` +- You cannot edit the Component ID or the Device ID +- [reserved](#reserved) fields can not be removed +- if a change is found an entry will be written to the eventlog. + + +## API + +Component details are available via the API. +Please see the [API-Docs](http://docs.librenms.org/API/API-Docs/#api-route-25) for details. + +## Alerting + +A default alerting rule exists that will raise an alert for any component which has a status of 0. +This can be disabled: + +- Globally - by removing the 'Component Alert' alerting rule. +- Locally - by setting the ignore field of an individual component to 1. + +The data that is written to each alert when it is raised is in the following format: + +`COMPONENT_TYPE - LABEL - ERROR` + +if you are creating a poller module which can detect a fault condition simply set STATUS to 0 and ERROR to a message that indicates the problem. + +# Example Code + +To see an example of how the component module can used, please see the following modules: + +- Cisco CBQoS + - includes/discovery/cisco-cbqos.inc.php + - includes/poller/cisco-cbqos.inc.php + - html/includes/graphs/device/cbqos_traffic.inc.php +- Cisco OTV + - includes/discovery/cisco-otv.inc.php + - includes/poller/applications/cisco-otv.inc.php + - html/includes/graphs/device/cisco-otv-mac.inc.php + - html/pages/device/apps/cisco-otv.inc.php + diff --git a/html/api_v0.php b/html/api_v0.php index 426591162..c77c0066b 100644 --- a/html/api_v0.php +++ b/html/api_v0.php @@ -49,6 +49,8 @@ $app->group( // api/v0/devices/$hostname/graphs $app->get('/:hostname/ports', 'authToken', 'get_port_graphs')->name('get_port_graphs'); // api/v0/devices/$hostname/ports + $app->get('/:hostname/components', 'authToken', 'get_components')->name('get_components'); + // api/v0/devices/$hostname/components $app->get('/:hostname/:type', 'authToken', 'get_graph_generic_by_hostname')->name('get_graph_generic_by_hostname'); // api/v0/devices/$hostname/$type $app->get('/:hostname/ports/:ifname', 'authToken', 'get_port_stats_by_port_hostname')->name('get_port_stats_by_port_hostname'); diff --git a/html/includes/api_functions.inc.php b/html/includes/api_functions.inc.php index 8469e1053..5dc4aee4c 100644 --- a/html/includes/api_functions.inc.php +++ b/html/includes/api_functions.inc.php @@ -13,6 +13,7 @@ */ require_once '../includes/functions.php'; +require_once '../includes/component.php'; function authToken(\Slim\Route $route) { @@ -500,6 +501,59 @@ function get_graph_by_portgroup() { } +function get_components() { + global $config; + $code = 200; + $status = 'ok'; + $message = ''; + $app = \Slim\Slim::getInstance(); + $router = $app->router()->getCurrentRoute()->getParams(); + $hostname = $router['hostname']; + + // Do some filtering if the user requests. + $options = array(); + if (isset($_GET['type'])) { + // set a type = filter + $options['filter']['type'] = array('=',$_GET['type']); + } + if (isset($_GET['id'])) { + // set a id = filter + $options['filter']['id'] = array('=',$_GET['id']); + } + if (isset($_GET['label'])) { + // set a label like filter + $options['filter']['label'] = array('LIKE',$_GET['label']); + } + if (isset($_GET['status'])) { + // set a status = filter + $options['filter']['status'] = array('=',$_GET['status']); + } + if (isset($_GET['disabled'])) { + // set a disabled = filter + $options['filter']['disabled'] = array('=',$_GET['disabled']); + } + if (isset($_GET['ignore'])) { + // set a ignore = filter + $options['filter']['ignore'] = array('=',$_GET['ignore']); + } + + // use hostname as device_id if it's all digits + $device_id = ctype_digit($hostname) ? $hostname : getidbyname($hostname); + $COMPONENT = new component(); + $components = $COMPONENT->getComponents($device_id,$options); + + $output = array( + 'status' => "$status", + 'err-msg' => $message, + 'count' => count($components[$device_id]), + 'components' => $components[$device_id], + ); + $app->response->setStatus($code); + $app->response->headers->set('Content-Type', 'application/json'); + echo _json_encode($output); +} + + function get_graphs() { global $config; $code = 200; diff --git a/html/includes/forms/component.inc.php b/html/includes/forms/component.inc.php new file mode 100644 index 000000000..6492bbfb5 --- /dev/null +++ b/html/includes/forms/component.inc.php @@ -0,0 +1,84 @@ + 'error', + 'message' => 'Need to be admin', + ); + echo _json_encode($response); + exit; +} + +$status = 'error'; +$message = 'Error with config'; + +// enable/disable components on devices. +$device_id = intval($_POST['device']); + +require_once "../includes/component.php"; +$OBJCOMP = new component(); + +// Go get the component array. +$COMPONENTS = $OBJCOMP->getComponents($device_id); + +// We only care about our device id. +$COMPONENTS = $COMPONENTS[$device_id]; + +// Track how many updates we are making. +$UPDATE = array(); + +foreach ($COMPONENTS as $ID => $AVP) { + // Is the component disabled? + if (isset($_POST['dis_'.$ID])) { + // Yes it is, was it disabled before? + if ($COMPONENTS[$ID]['disabled'] == 0) { + // No it wasn't, best we disable it then.. + $COMPONENTS[$ID]['disabled'] = 1; + $UPDATE[$ID] = true; + } + } + else { + // No its not, was it disabled before? + if ($COMPONENTS[$ID]['disabled'] == 1) { + // Yes it was, best we enable it then.. + $COMPONENTS[$ID]['disabled'] = 0; + $UPDATE[$ID] = true; + } + } + + // Is the component ignored? + if (isset($_POST['ign_'.$ID])) { + // Yes it is, was it ignored before? + if ($COMPONENTS[$ID]['ignore'] == 0) { + // No it wasn't, best we ignore it then.. + $COMPONENTS[$ID]['ignore'] = 1; + $UPDATE[$ID] = true; + } + } + else { + // No its not, was it ignored before? + if ($COMPONENTS[$ID]['ignore'] == 1) { + // Yes it was, best we un-ignore it then.. + $COMPONENTS[$ID]['ignore'] = 0; + $UPDATE[$ID] = true; + } + } +} + +if (count($UPDATE) > 0) { + // Update our edited components. + $STATUS = $OBJCOMP->setComponentPrefs($device_id,$COMPONENTS); + + $message = count($UPDATE).' Device records updated.'; + $status = 'ok'; +} +else { + $message = 'Record unchanged. No update necessary.'; + $status = 'ok'; +} + +$response = array( + 'status' => $status, + 'message' => $message, +); +echo _json_encode($response); \ No newline at end of file diff --git a/html/includes/functions.inc.php b/html/includes/functions.inc.php index b30325590..9ed2cac9d 100644 --- a/html/includes/functions.inc.php +++ b/html/includes/functions.inc.php @@ -1156,6 +1156,16 @@ function alert_details($details) { $fallback = false; } + if ($tmp_alerts['type'] && $tmp_alerts['label']) { + if ($tmp_alerts['error'] == "") { + $fault_detail .= ' '.$tmp_alerts['type'].' - '.$tmp_alerts['label'].'; '; + } + else { + $fault_detail .= ' '.$tmp_alerts['type'].' - '.$tmp_alerts['label'].' - '.$tmp_alerts['error'].'; '; + } + $fallback = false; + } + if ($fallback === true) { foreach ($tmp_alerts as $k => $v) { if (!empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) { diff --git a/html/includes/table/component.inc.php b/html/includes/table/component.inc.php new file mode 100644 index 000000000..7a0873648 --- /dev/null +++ b/html/includes/table/component.inc.php @@ -0,0 +1,59 @@ +getComponents($device_id,$options); + +$response[] = array( + 'id' => '', + 'label' => ' ', + 'status' => '', + 'disable' => '', + 'ignore' => '', +); + +foreach ($COMPONENTS[$device_id] as $ID => $AVP) { + $response[] = array( + 'id' => $ID, + 'type' => $AVP['type'], + 'label' => $AVP['label'], + 'status' => ($AVP['status'] ? "Normal" : "Alert"), + 'disable' => '', + 'ignore' => '', + ); +}//end foreach + +$output = array( + 'current' => $current, + 'rowCount' => $rowCount, + 'rows' => $response, + 'total' => count($COMPONENTS[$device_id]), +); +echo _json_encode($output); \ No newline at end of file diff --git a/html/pages/device/edit.inc.php b/html/pages/device/edit.inc.php index fe8a9c259..e866e4504 100644 --- a/html/pages/device/edit.inc.php +++ b/html/pages/device/edit.inc.php @@ -38,6 +38,8 @@ else { $panes['storage'] = 'Storage'; $panes['misc'] = 'Misc'; + $panes['component'] = 'Components'; + print_optionbar_start(); unset($sep); diff --git a/html/pages/device/edit/component.inc.php b/html/pages/device/edit/component.inc.php new file mode 100644 index 000000000..aebfac294 --- /dev/null +++ b/html/pages/device/edit/component.inc.php @@ -0,0 +1,102 @@ +
n.b For the first time, please click any button twice.
+ +
+ + + + + + + + + + + +
IDTypeLabelStatusDisableIgnore
+ + + '> +
+ diff --git a/includes/component.php b/includes/component.php new file mode 100644 index 000000000..ee5a93774 --- /dev/null +++ b/includes/component.php @@ -0,0 +1,254 @@ + + * + * 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. Please see LICENSE.txt at the top level of + * the source code distribution for details. + */ + +class component { + /* + * These fields are used in the component table. They are returned in the array + * so that they can be modified but they can not be set as user attributes. We + * also set their default values. + */ + private $reserved = array( + 'type' => '', + 'label' => '', + 'status' => 1, + 'ignore' => 0, + 'disabled' => 0, + 'error' => '', + ); + + public function getComponentType($TYPE=null) { + if (is_null($TYPE)) { + $SQL = "SELECT DISTINCT `type` as `name` FROM `component` ORDER BY `name`"; + $row = dbFetchRow($SQL, array()); + } + else { + $SQL = "SELECT DISTINCT `type` as `name` FROM `component` WHERE `type` = ? ORDER BY `name`"; + $row = dbFetchRow($SQL, array($TYPE)); + } + + if (!isset($row)) { + // We didn't find any component types + return false; + } + else { + // We found some.. + return $row; + } + } + + public function getComponents($device_id=null,$options=array()) { + // Define our results array, this will be set even if no rows are returned. + $RESULT = array(); + $PARAM = array(); + + // Our base SQL Query, with no options. + $SQL = "SELECT `C`.`id`,`C`.`device_id`,`C`.`type`,`C`.`label`,`C`.`status`,`C`.`disabled`,`C`.`ignore`,`C`.`error`,`CP`.`attribute`,`CP`.`value` FROM `component` as `C` LEFT JOIN `component_prefs` as `CP` on `C`.`id`=`CP`.`component` WHERE "; + + // Device_id is shorthand for filter C.device_id = $device_id. + if (!is_null($device_id)) { + $options['filter']['device_id'] = array('=', $device_id); + } + + // Type is shorthand for filter type = $type. + if (isset($options['type'])) { + $options['filter']['type'] = array('=', $options['type']); + } + + // filter field => array(operator,value) + // Filters results based on the field, operator and value + $COUNT = 0; + if (isset($options['filter'])) { + $COUNT++; + $SQL .= " ( "; + foreach ($options['filter'] as $field => $array) { + if ($array[0] == 'LIKE') { + $SQL .= "`".$field."` LIKE ? AND "; + $array[1] = "%".$array[1]."%"; + } + else { + // Equals operator is the default + $SQL .= "`".$field."` = ? AND "; + } + array_push($PARAM,$array[1]); + } + // Strip the last " AND " before closing the bracket. + $SQL = substr($SQL,0,-5)." )"; + } + + if ($COUNT == 0) { + // Strip the " WHERE " that we didn't use. + $SQL = substr($SQL,0,-7); + } + + // sort column direction + // Add SQL sorting to the results + if (isset($options['sort'])) { + $SQL .= " ORDER BY ".$options['sort']; + } + + // Get our component records using our built SQL. + $COMPONENTS = dbFetchRows($SQL, $PARAM); + + // if we have no components we need to return nothing + if (count($COMPONENTS) == 0) { + return $RESULT; + } + + // Add the AVP's to the array. + foreach ($COMPONENTS as $COMPONENT) { + if ($COMPONENT['attribute'] != "") { + // if this component has attributes, set them in the array. + $RESULT[$COMPONENT['device_id']][$COMPONENT['id']][$COMPONENT['attribute']] = $COMPONENT['value']; + } + } + + // Populate our reserved fields into the Array, these cant be used as user attributes. + foreach ($COMPONENTS as $COMPONENT) { + foreach ($this->reserved as $k => $v) { + $RESULT[$COMPONENT['device_id']][$COMPONENT['id']][$k] = $COMPONENT[$k]; + } + + // Sort each component array so the attributes are in order. + ksort($RESULT[$RESULT[$COMPONENT['device_id']][$COMPONENT['id']]]); + ksort($RESULT[$RESULT[$COMPONENT['device_id']]]); + } + + // limit array(start,count) + if (isset($options['limit'])) { + $TEMP = array(); + $COUNT = 0; + // k = device_id, v = array of components for that device_id + foreach ($RESULT as $k => $v) { + // k1 = component id, v1 = component array + foreach ($v as $k1 => $v1) { + if ( ($COUNT >= $options['limit'][0]) && ($COUNT < $options['limit'][0]+$options['limit'][1])) { + $TEMP[$k][$k1] = $v1; + } + // We are counting components. + $COUNT++; + } + } + $RESULT = $TEMP; + } + + return $RESULT; + } + + public function createComponent ($device_id,$TYPE) { + // Prepare our default values to be inserted. + $DATA = $this->reserved; + + // Add the device_id and type + $DATA['device_id'] = $device_id; + $DATA['type'] = $TYPE; + + // Insert a new component into the database. + $id = dbInsert($DATA, 'component'); + + // Create a default component array based on what was inserted. + $ARRAY[$id] = $DATA; + unset ($ARRAY[$id]['device_id']); // This doesn't belong here. + return $ARRAY; + } + + public function deleteComponent ($id) { + // Delete a component from the database. + return dbDelete('component', "`id` = ?",array($id)); + } + + public function setComponentPrefs ($device_id,$ARRAY) { + // Compare the arrays. Update/Insert where necessary. + + $OLD = $this->getComponents($device_id); + // Loop over each component. + foreach ($ARRAY as $COMPONENT => $AVP) { + + // Make sure the component already exists. + if (!isset($OLD[$device_id][$COMPONENT])) { + // Error. Component doesn't exist in the database. + continue; + } + + // Ignore type, we cant change that. + unset($AVP['type'],$OLD[$device_id][$COMPONENT]['type']); + + // Process our reserved components first. + $UPDATE = array(); + foreach ($this->reserved as $k => $v) { + // does the reserved field exist, if not skip. + if (isset($AVP[$k])) { + + // Has the value changed? + if ($AVP[$k] != $OLD[$device_id][$COMPONENT][$k]) { + // The value has been modified, add it to our update array. + $UPDATE[$k] = $AVP[$k]; + } + + // Unset the reserved field. We don't want to insert it below. + unset($AVP[$k],$OLD[$device_id][$COMPONENT][$k]); + } + } + + // Has anything changed, do we need to update? + if (count($UPDATE) > 0) { + // We have data to update + dbUpdate($UPDATE, 'component', '`id` = ?', array($COMPONENT)); + + // Log the update to the Eventlog. + $MSG = "Component ".$COMPONENT." has been modified: "; + foreach ($UPDATE as $k => $v) { + $MSG .= $k." => ".$v.","; + } + $MSG = substr($MSG,0,-1); + log_event($MSG,$device_id,'component',$COMPONENT); + } + + // Process our AVP Adds and Updates + foreach ($AVP as $ATTR => $VALUE) { + // We have our AVP, lets see if we need to do anything with it. + + if (!isset($OLD[$device_id][$COMPONENT][$ATTR])) { + // We have a newly added attribute, need to insert into the DB + $DATA = array('component'=>$COMPONENT, 'attribute'=>$ATTR, 'value'=>$VALUE); + $id = dbInsert($DATA, 'component_prefs'); + + // Log the addition to the Eventlog. + log_event ("Component: " . $AVP[$COMPONENT]['type'] . "(" . $COMPONENT . "). Attribute: " . $ATTR . ", was added with value: " . $VALUE, $device_id, 'component', $COMPONENT); + } + elseif ($OLD[$device_id][$COMPONENT][$ATTR] != $VALUE) { + // Attribute exists but the value is different, need to update + $DATA = array('value'=>$VALUE); + dbUpdate($DATA, 'component_prefs', '`component` = ? AND `attribute` = ?', array($COMPONENT, $ATTR)); + + // Add the modification to the Eventlog. + log_event("Component: ".$AVP[$COMPONENT]['type']."(".$COMPONENT."). Attribute: ".$ATTR.", was modified from: ".$OLD[$COMPONENT][$ATTR].", to: ".$VALUE,$device_id,'component',$COMPONENT); + } + + } // End Foreach COMPONENT + + // Process our Deletes. + $DELETE = array_diff_key($OLD[$device_id][$COMPONENT], $AVP); + foreach ($DELETE as $KEY => $VALUE) { + // As the Attribute has been removed from the array, we should remove it from the database. + dbDelete('component_prefs', "`component` = ? AND `attribute` = ?",array($COMPONENT,$KEY)); + + // Log the addition to the Eventlog. + log_event ("Component: " . $AVP[$COMPONENT]['type'] . "(" . $COMPONENT . "). Attribute: " . $ATTR . ", was deleted.", $COMPONENT); + } + + } + + return true; + } + +} \ No newline at end of file diff --git a/sql-schema/084.sql b/sql-schema/084.sql new file mode 100644 index 000000000..cb4f4165f --- /dev/null +++ b/sql-schema/084.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS `component`; +CREATE TABLE `component` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID for each component, unique index', `device_id` int(11) unsigned NOT NULL COMMENT 'device_id from the devices table', `type` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT 'name from the component_type table', `label` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Display label for the component', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'The status of the component, retreived from the device', `disabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Should this component be polled', `ignore` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Should this component be alerted on', `error` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Error message if in Alert state', PRIMARY KEY (`id`), KEY `device` (`device_id`), KEY `type` (`type`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='components attached to a device.'; +DROP TABLE IF EXISTS `component_prefs`; +CREATE TABLE `component_prefs` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID for each entry', `component` int(11) unsigned NOT NULL COMMENT 'id from the component table', `attribute` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Attribute for the Component', `value` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Value for the Component', PRIMARY KEY (`id`), KEY `component` (`component`), CONSTRAINT `component_prefs_ibfk_1` FOREIGN KEY (`component`) REFERENCES `component` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='AV Pairs for each component'; +INSERT INTO `alert_rules` (`device_id`,`rule`,`severity`,`extra`,`disabled`,`name`) VALUES ('-1','%macros.component_alert = \"1\"','critical','{\"mute\":false,\"count\":\"-1\",\"delay\":\"300\"}',0,'Component Alert'); +INSERT INTO `config` (`config_name`,`config_value`,`config_default`,`config_descr`,`config_group`,`config_group_order`,`config_sub_group`,`config_sub_group_order`,`config_hidden`,`config_disabled`) VALUES ('alert.macros.rule.component','(%component.disabled = 0 && %component.ignore = 0)','(%component.disabled = 0 && %component.ignore = 0)','Component that isn\'t disabled or ignored','alerting',0,'macros',0,'1','0'),('alert.macros.rule.component_normal','(%component.status = 1 && %macros.component)','(%component.status = 1 && %macros.component)','Component that is in a normal state','alerting',0,'macros',0,'1','0'),('alert.macros.rule.component_alert','(%component.status = 0 && %macros.component)','(%component.status = 0 && %macros.component)','Component that alerting','alerting',0,'macros',0,'1','0'); \ No newline at end of file From c6be2a8ad72d2c394c8e6fd5cc025262584eaae1 Mon Sep 17 00:00:00 2001 From: Aaron Daniels Date: Tue, 22 Dec 2015 11:05:42 +1000 Subject: [PATCH 2/9] - Removed default alerting rules, user can create their own. - Updated documentation --- doc/Extensions/Component.md | 22 +++++++++++++++------- sql-schema/084.sql | 1 - 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/doc/Extensions/Component.md b/doc/Extensions/Component.md index 2837db899..235335380 100644 --- a/doc/Extensions/Component.md +++ b/doc/Extensions/Component.md @@ -281,18 +281,26 @@ Please see the [API-Docs](http://docs.librenms.org/API/API-Docs/#api-route-25) f ## Alerting -A default alerting rule exists that will raise an alert for any component which has a status of 0. -This can be disabled: - -- Globally - by removing the 'Component Alert' alerting rule. -- Locally - by setting the ignore field of an individual component to 1. +It is intended that discovery/poller modules will detect the status of a component during the polling cycle. +If you are creating a poller module which can detect a fault condition simply set STATUS to 0 and ERROR to a message that indicates the problem. + + +To actually raise an alert, the user will need to create an alert rule. To assist with this several Alerting Macro's have been created: + +- ```%macro.component_alert``` - A component that is not disabled or ignored and in alert state. +- ```%macro.component_normal``` - A component that is not disabled or ignored and NOT in alert state. + +To raise alerts for components, the following rules could be created: + +- ```%macros.component_alert = "1"``` - To alert on all components +- ```%macros.component_alert = "1" && %component.type = ""``` - To alert on all components of a particular type. + +If there is a particular component you would like excluded from alerting, simply set the ignore field to 1. The data that is written to each alert when it is raised is in the following format: `COMPONENT_TYPE - LABEL - ERROR` -if you are creating a poller module which can detect a fault condition simply set STATUS to 0 and ERROR to a message that indicates the problem. - # Example Code To see an example of how the component module can used, please see the following modules: diff --git a/sql-schema/084.sql b/sql-schema/084.sql index cb4f4165f..458b8d92d 100644 --- a/sql-schema/084.sql +++ b/sql-schema/084.sql @@ -2,5 +2,4 @@ DROP TABLE IF EXISTS `component`; CREATE TABLE `component` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID for each component, unique index', `device_id` int(11) unsigned NOT NULL COMMENT 'device_id from the devices table', `type` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT 'name from the component_type table', `label` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Display label for the component', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'The status of the component, retreived from the device', `disabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Should this component be polled', `ignore` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Should this component be alerted on', `error` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Error message if in Alert state', PRIMARY KEY (`id`), KEY `device` (`device_id`), KEY `type` (`type`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='components attached to a device.'; DROP TABLE IF EXISTS `component_prefs`; CREATE TABLE `component_prefs` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID for each entry', `component` int(11) unsigned NOT NULL COMMENT 'id from the component table', `attribute` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Attribute for the Component', `value` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Value for the Component', PRIMARY KEY (`id`), KEY `component` (`component`), CONSTRAINT `component_prefs_ibfk_1` FOREIGN KEY (`component`) REFERENCES `component` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='AV Pairs for each component'; -INSERT INTO `alert_rules` (`device_id`,`rule`,`severity`,`extra`,`disabled`,`name`) VALUES ('-1','%macros.component_alert = \"1\"','critical','{\"mute\":false,\"count\":\"-1\",\"delay\":\"300\"}',0,'Component Alert'); INSERT INTO `config` (`config_name`,`config_value`,`config_default`,`config_descr`,`config_group`,`config_group_order`,`config_sub_group`,`config_sub_group_order`,`config_hidden`,`config_disabled`) VALUES ('alert.macros.rule.component','(%component.disabled = 0 && %component.ignore = 0)','(%component.disabled = 0 && %component.ignore = 0)','Component that isn\'t disabled or ignored','alerting',0,'macros',0,'1','0'),('alert.macros.rule.component_normal','(%component.status = 1 && %macros.component)','(%component.status = 1 && %macros.component)','Component that is in a normal state','alerting',0,'macros',0,'1','0'),('alert.macros.rule.component_alert','(%component.status = 0 && %macros.component)','(%component.status = 0 && %macros.component)','Component that alerting','alerting',0,'macros',0,'1','0'); \ No newline at end of file From 0453c235449fbadc0001f7da4fd77d71f06a18cd Mon Sep 17 00:00:00 2001 From: Aaron Daniels Date: Tue, 22 Dec 2015 11:06:30 +1000 Subject: [PATCH 3/9] - Moved SQL to reflect upstream changes. --- sql-schema/{084.sql => 085.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sql-schema/{084.sql => 085.sql} (100%) diff --git a/sql-schema/084.sql b/sql-schema/085.sql similarity index 100% rename from sql-schema/084.sql rename to sql-schema/085.sql From 87e88dacfd22d7aaa769761f796ce6a018ed7c01 Mon Sep 17 00:00:00 2001 From: Aaron Daniels Date: Thu, 14 Jan 2016 05:22:37 +1000 Subject: [PATCH 4/9] - Compress API filters to a loop - Check for filters for valid fields --- html/includes/api_functions.inc.php | 24 +++++------------------- includes/component.php | 20 ++++++++++++-------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/html/includes/api_functions.inc.php b/html/includes/api_functions.inc.php index 0e50b65db..59527f843 100644 --- a/html/includes/api_functions.inc.php +++ b/html/includes/api_functions.inc.php @@ -519,29 +519,15 @@ function get_components() { // Do some filtering if the user requests. $options = array(); - if (isset($_GET['type'])) { - // set a type = filter - $options['filter']['type'] = array('=',$_GET['type']); - } - if (isset($_GET['id'])) { - // set a id = filter - $options['filter']['id'] = array('=',$_GET['id']); - } + // We need to specify the label as this is a LIKE query if (isset($_GET['label'])) { // set a label like filter $options['filter']['label'] = array('LIKE',$_GET['label']); + unset ($_GET['label']); } - if (isset($_GET['status'])) { - // set a status = filter - $options['filter']['status'] = array('=',$_GET['status']); - } - if (isset($_GET['disabled'])) { - // set a disabled = filter - $options['filter']['disabled'] = array('=',$_GET['disabled']); - } - if (isset($_GET['ignore'])) { - // set a ignore = filter - $options['filter']['ignore'] = array('=',$_GET['ignore']); + // Add the rest of the options with an equals query + foreach ($_GET as $k) { + $options['filter'][$k] = array('=',$_GET[$k]); } // use hostname as device_id if it's all digits diff --git a/includes/component.php b/includes/component.php index ee5a93774..6808e847b 100644 --- a/includes/component.php +++ b/includes/component.php @@ -69,17 +69,21 @@ class component { $COUNT = 0; if (isset($options['filter'])) { $COUNT++; + $validFields = array('device_id','type','id','label','status','disabled','ignore','error'); $SQL .= " ( "; foreach ($options['filter'] as $field => $array) { - if ($array[0] == 'LIKE') { - $SQL .= "`".$field."` LIKE ? AND "; - $array[1] = "%".$array[1]."%"; + // Only add valid fields to the query + if (in_array($field,$validFields)) { + if ($array[0] == 'LIKE') { + $SQL .= "`".$field."` LIKE ? AND "; + $array[1] = "%".$array[1]."%"; + } + else { + // Equals operator is the default + $SQL .= "`".$field."` = ? AND "; + } + array_push($PARAM,$array[1]); } - else { - // Equals operator is the default - $SQL .= "`".$field."` = ? AND "; - } - array_push($PARAM,$array[1]); } // Strip the last " AND " before closing the bracket. $SQL = substr($SQL,0,-5)." )"; From 29ae6aaaaeeb0a4bce60f169203b59992729cb31 Mon Sep 17 00:00:00 2001 From: Aaron Daniels Date: Thu, 14 Jan 2016 06:10:00 +1000 Subject: [PATCH 5/9] - Move 085.sql to 089.sql --- sql-schema/{085.sql => 089.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sql-schema/{085.sql => 089.sql} (100%) diff --git a/sql-schema/085.sql b/sql-schema/089.sql similarity index 100% rename from sql-schema/085.sql rename to sql-schema/089.sql From f61838a61730b50ea87b86a04059cf894e0dd5b4 Mon Sep 17 00:00:00 2001 From: Aaron Daniels Date: Fri, 15 Jan 2016 17:52:12 +1000 Subject: [PATCH 6/9] - Moved sql beacuse of upstream changes --- sql-schema/{089.sql => 091.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sql-schema/{089.sql => 091.sql} (100%) diff --git a/sql-schema/089.sql b/sql-schema/091.sql similarity index 100% rename from sql-schema/089.sql rename to sql-schema/091.sql From 2a59034193003768b3c389283d874e8f9bf7598f Mon Sep 17 00:00:00 2001 From: Aaron Daniels Date: Fri, 15 Jan 2016 16:54:02 +1000 Subject: [PATCH 7/9] - Resolved some issues identified by scrutiniser. --- includes/component.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/component.php b/includes/component.php index 6808e847b..180f0a8da 100644 --- a/includes/component.php +++ b/includes/component.php @@ -160,6 +160,7 @@ class component { $id = dbInsert($DATA, 'component'); // Create a default component array based on what was inserted. + $ARRAY = array(); $ARRAY[$id] = $DATA; unset ($ARRAY[$id]['device_id']); // This doesn't belong here. return $ARRAY; @@ -224,7 +225,7 @@ class component { if (!isset($OLD[$device_id][$COMPONENT][$ATTR])) { // We have a newly added attribute, need to insert into the DB $DATA = array('component'=>$COMPONENT, 'attribute'=>$ATTR, 'value'=>$VALUE); - $id = dbInsert($DATA, 'component_prefs'); + dbInsert($DATA, 'component_prefs'); // Log the addition to the Eventlog. log_event ("Component: " . $AVP[$COMPONENT]['type'] . "(" . $COMPONENT . "). Attribute: " . $ATTR . ", was added with value: " . $VALUE, $device_id, 'component', $COMPONENT); @@ -247,7 +248,7 @@ class component { dbDelete('component_prefs', "`component` = ? AND `attribute` = ?",array($COMPONENT,$KEY)); // Log the addition to the Eventlog. - log_event ("Component: " . $AVP[$COMPONENT]['type'] . "(" . $COMPONENT . "). Attribute: " . $ATTR . ", was deleted.", $COMPONENT); + log_event ("Component: " . $AVP[$COMPONENT]['type'] . "(" . $COMPONENT . "). Attribute: " . $KEY . ", was deleted.", $COMPONENT); } } From 582c3984e5f442e3a09e7aca33fe19c06cac10bf Mon Sep 17 00:00:00 2001 From: wiad Date: Tue, 19 Jan 2016 10:00:14 +0100 Subject: [PATCH 8/9] Try to find rancid file based on both fqdn and short host name --- html/pages/device/showconfig.inc.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/html/pages/device/showconfig.inc.php b/html/pages/device/showconfig.inc.php index 675e43674..410cc2f27 100644 --- a/html/pages/device/showconfig.inc.php +++ b/html/pages/device/showconfig.inc.php @@ -16,7 +16,15 @@ if ($_SESSION['userlevel'] >= '7') { if (is_file($configs.$device['hostname'])) { $file = $configs.$device['hostname']; - } + } elseif (is_file($configs.strtok($device['hostname'], '.'))) { // Strip domain + $file = $configs.strtok($device['hostname'], '.'); + } else { + if (!empty($config['mydomain'])) { // Try with domain name if set + if (is_file($configs.$device['hostname'].'.'.$config['mydomain'])) { + $file = $configs.$device['hostname'].'.'.$config['mydomain']; + } + } + } // end if } echo '
'; From dda947449af51ec972a35cd924a2aad18fdaf197 Mon Sep 17 00:00:00 2001 From: wiad Date: Tue, 19 Jan 2016 10:11:11 +0100 Subject: [PATCH 9/9] fix if statement style --- html/pages/device/showconfig.inc.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/html/pages/device/showconfig.inc.php b/html/pages/device/showconfig.inc.php index 410cc2f27..9e5d0b1a6 100644 --- a/html/pages/device/showconfig.inc.php +++ b/html/pages/device/showconfig.inc.php @@ -16,9 +16,11 @@ if ($_SESSION['userlevel'] >= '7') { if (is_file($configs.$device['hostname'])) { $file = $configs.$device['hostname']; - } elseif (is_file($configs.strtok($device['hostname'], '.'))) { // Strip domain + } + elseif (is_file($configs.strtok($device['hostname'], '.'))) { // Strip domain $file = $configs.strtok($device['hostname'], '.'); - } else { + } + else { if (!empty($config['mydomain'])) { // Try with domain name if set if (is_file($configs.$device['hostname'].'.'.$config['mydomain'])) { $file = $configs.$device['hostname'].'.'.$config['mydomain'];