diff --git a/doc/API/API-Docs.md b/doc/API/API-Docs.md
index e879fe637b..6b6e0afbb1 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 0000000000..2837db8993
--- /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 426591162b..c77c0066bf 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 8469e10532..5dc4aee4cb 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 0000000000..6492bbfb5b
--- /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 b303255908..9ed2cac9d1 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 0000000000..7a0873648d
--- /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 fe8a9c2593..e866e4504b 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 0000000000..aebfac2941
--- /dev/null
+++ b/html/pages/device/edit/component.inc.php
@@ -0,0 +1,102 @@
+