refactor: Improve yaml state discovery (#7221)

* feature: Improve yaml state discovery
Handle state values that are returned as strings instead of int
Synchronize state values for existing state translations so we can change them without creating a new translation and losing historical data
More extensive/verbose yaml discovery phpunit tests
dbBulkInsert, use the first entry instead of requiring the first entry to be at index 0

* Update sensor state documentation
re-order values for better readability
remove os check
Use snmpwalk_group since it is more flexible

* Add some more debug output in dynamic discovery
This commit is contained in:
Tony Murray
2017-09-03 13:58:39 -05:00
committed by Neil Lathwood
parent 5441bafc81
commit 7b262a6851
5 changed files with 180 additions and 96 deletions

View File

@@ -97,12 +97,12 @@ modules:
index: '{{ $index }}'
state_name: otherStateSensorErrorStatus
states:
- { descr: normal, graph: 0, value: 0, generic: 0 }
- { descr: info, graph: 0, value: 1, generic: 1 }
- { descr: warning, graph: 0, value: 2, generic: 1 }
- { descr: error, graph: 0, value: 3, generic: 2 }
- { descr: critical, graph: 0, value: 4, generic: 2 }
- { descr: failure, graph: 0, value: 5, generic: 2 }
- { value: 0, generic: 0, graph: 0, descr: normal }
- { value: 1, generic: 1, graph: 0, descr: info }
- { value: 2, generic: 1, graph: 0, descr: warning }
- { value: 3, generic: 2, graph: 0, descr: error }
- { value: 4, generic: 2, graph: 0, descr: critical }
- { value: 5, generic: 2, graph: 0, descr: failure }
```
@@ -114,45 +114,28 @@ The file should be located in /includes/discovery/sensors/state/cisco.inc.php.
```php
<?php
if ($device['os_group'] == 'cisco') {
$oids = snmpwalk_cache_multi_oid($device, 'ciscoEnvMonSupplyStatusTable', array(), 'CISCO-ENVMON-MIB');
$cur_oid = '.1.3.6.1.4.1.9.9.13.1.5.1.3.';
$oids = snmpwalk_group($device, 'ciscoEnvMonSupplyStatusTable', 'CISCO-ENVMON-MIB');
if (is_array($oids)) {
if (!empty($oids)) {
//Create State Index
$state_name = 'ciscoEnvMonSupplyState';
$states = array(
array('value' => 1, 'generic' => 0, 'graph' => 0, 'descr' => 'normal'),
array('value' => 2, 'generic' => 1, 'graph' => 0, 'descr' => 'warning'),
array('value' => 3, 'generic' => 2, 'graph' => 0, 'descr' => 'critical'),
array('value' => 4, 'generic' => 3, 'graph' => 0, 'descr' => 'shutdown'),
array('value' => 5, 'generic' => 3, 'graph' => 0, 'descr' => 'notPresent'),
array('value' => 6, 'generic' => 2, 'graph' => 0, 'descr' => 'notFunctioning'),
);
create_state_index($state_name, $states);
//Create State Index
$state_name = 'ciscoEnvMonSupplyState';
$state_index_id = create_state_index($state_name);
$num_oid = '.1.3.6.1.4.1.9.9.13.1.5.1.3.';
foreach ($oids as $index => $entry) {
//Discover Sensors
discover_sensor($valid['sensor'], 'state', $device, $num_oid.$index, $index, $state_name, $entry['ciscoEnvMonSupplyStatusDescr'], '1', '1', null, null, null, null, $entry['ciscoEnvMonSupplyState'], 'snmp', $index);
//Create State Translation
if ($state_index_id) {
$states = array(
array($state_index_id,'normal',0,1,0) ,
array($state_index_id,'warning',0,2,1) ,
array($state_index_id,'critical',0,3,2) ,
array($state_index_id,'shutdown',0,4,3) ,
array($state_index_id,'notPresent',0,5,3) ,
array($state_index_id,'notFunctioning',0,6,2)
);
foreach($states as $value){
$insert = array(
'state_index_id' => $value[0],
'state_descr' => $value[1],
'state_draw_graph' => $value[2],
'state_value' => $value[3],
'state_generic_value' => $value[4]
);
dbInsert($insert, 'state_translations');
}
}
foreach ($oids as $index => $entry) {
//Discover Sensors
discover_sensor($valid['sensor'], 'state', $device, $cur_oid.$index, $index, $state_name, $entry['ciscoEnvMonSupplyStatusDescr'], '1', '1', null, null, null, null, $entry['ciscoEnvMonSupplyState'], 'snmp', $index);
//Create Sensor To State Index
create_sensor_to_state_index($device, $state_name, $index);
}
//Create Sensor To State Index
create_sensor_to_state_index($device, $state_name, $index);
}
}
```

View File

@@ -184,14 +184,17 @@ function dbBulkInsert($data, $table)
$data = $table;
$table = $tmp;
}
if (count($data) === 0) {
// check that data isn't an empty array
if (empty($data)) {
return false;
}
if (count($data[0]) === 0) {
// make sure we have fields to insert
$fields = array_keys(reset($data));
if (empty($fields)) {
return false;
}
$sql = 'INSERT INTO `'.$table.'` (`'.implode('`,`', array_keys($data[0])).'`) VALUES ';
$sql = 'INSERT INTO `'.$table.'` (`'.implode('`,`', $fields).'`) VALUES ';
$values ='';
foreach ($data as $row) {

View File

@@ -1081,12 +1081,33 @@ function discovery_process(&$valid, $device, $sensor_type, $pre_cache)
if (isset($device['dynamic_discovery']['modules']['sensors'][$sensor_type]['options'])) {
$sensor_options = $device['dynamic_discovery']['modules']['sensors'][$sensor_type]['options'];
}
d_echo("Dynamic Discovery ($sensor_type): ");
d_echo($device['dynamic_discovery']['modules']['sensors'][$sensor_type]);
foreach ($device['dynamic_discovery']['modules']['sensors'][$sensor_type]['data'] as $data) {
$tmp_name = $data['oid'];
$raw_data = $pre_cache[$tmp_name];
$raw_data = (array)$pre_cache[$tmp_name];
$cached_data = $pre_cache['__cached'] ?: array();
d_echo("Data $tmp_name: ");
d_echo($raw_data);
foreach ($raw_data as $index => $snmp_data) {
$value = is_numeric($snmp_data[$data['value']]) ? $snmp_data[$data['value']] : (is_numeric($snmp_data[$data['oid']]) ? $snmp_data[$data['oid']]: false);
// get the value for this sensor, check 'value' and 'oid', if state string, translate to a number
$data_name = isset($data['value']) ? $data['value'] : $data['oid']; // fallback to oid if value is not set
if (is_numeric($snmp_data[$data_name])) {
$value = $snmp_data[$data_name];
} elseif ($sensor_type === 'state') {
// translate string states to values (poller does this as well)
$states = array_column($data['states'], 'value', 'descr');
$value = isset($states[$snmp_data[$data_name]]) ? $states[$snmp_data[$data_name]] : false;
} else {
$value = false;
}
d_echo("Final sensor value: $value\n");
if (can_skip_sensor($value, $data, $sensor_options) === false && is_numeric($value)) {
$oid = $data['num_oid'] . $index;
if (isset($snmp_data[$data['descr']])) {
@@ -1111,37 +1132,25 @@ function discovery_process(&$valid, $device, $sensor_type, $pre_cache)
$low_warn_limit = is_numeric($data['low_warn_limit']) ? $data['low_warn_limit'] : ($snmp_data[$data['low_warn_limit']] ?: 'null');
$warn_limit = is_numeric($data['warn_limit']) ? $data['warn_limit'] : ($snmp_data[$data['warn_limit']] ?: 'null');
$high_limit = is_numeric($data['high_limit']) ? $data['high_limit'] : ($snmp_data[$data['high_limit']] ?: 'null');
$state_name = '';
if ($sensor_type !== 'state') {
$sensor_name = $device['os'];
if ($sensor_type === 'state') {
$sensor_name = $data['state_name'] ?: $data['oid'];
create_state_index($sensor_name, $data['states']);
} else {
if (is_numeric($divisor)) {
$value = $value / $divisor;
}
if (is_numeric($multiplier)) {
$value = $value * $multiplier;
}
} else {
$state_name = $data['state_name'] ?: $data['oid'];
$state_index_id = create_state_index($state_name);
if ($state_index_id != null) {
foreach ($data['states'] as $state) {
$insert = array(
'state_index_id' => $state_index_id,
'state_descr' => $state['descr'],
'state_draw_graph' => $state['graph'],
'state_value' => $state['value'],
'state_generic_value' => $state['generic']
);
dbInsert($insert, 'state_translations');
}
}
}
$tmp_index = $data['index'] ?: $index;
$uindex = str_replace('{{ $index }}', $index, $tmp_index);
$uindex = str_replace('{{ $index }}', $index, $data['index'] ?: $index);
discover_sensor($valid['sensor'], $sensor_type, $device, $oid, $uindex, $sensor_name, $descr, $divisor, $multiplier, $low_limit, $low_warn_limit, $warn_limit, $high_limit, $value);
if ($sensor_type === 'state') {
discover_sensor($valid['sensor'], $sensor_type, $device, $oid, $uindex, $state_name, $descr, $divisor, $multiplier, $low_limit, $low_warn_limit, $warn_limit, $high_limit, $value);
create_sensor_to_state_index($device, $state_name, $uindex);
} else {
discover_sensor($valid['sensor'], $sensor_type, $device, $oid, $uindex, $device['os'], $descr, $divisor, $multiplier, $low_limit, $low_warn_limit, $warn_limit, $high_limit, $value);
create_sensor_to_state_index($device, $sensor_name, $uindex);
}
}
}

View File

@@ -1586,19 +1586,87 @@ function rrdtest($path, &$stdOutput, &$stdError)
return $status['exitcode'];
}
function create_state_index($state_name)
/**
* Create a new state index. Update translations if $states is given.
*
* For for backward compatibility:
* Returns null if $states is empty, $state_name already exists, and contains state translations
*
* @param string $state_name the unique name for this state translation
* @param array $states array of states, each must contain keys: descr, graph, value, generic
* @return int|null
*/
function create_state_index($state_name, $states = array())
{
$state_index_id = dbFetchCell('SELECT `state_index_id` FROM state_indexes WHERE state_name = ? LIMIT 1', array($state_name));
if (!is_numeric($state_index_id)) {
$insert = array('state_name' => $state_name);
return dbInsert($insert, 'state_indexes');
} else {
$state_index_id = dbInsert(array('state_name' => $state_name), 'state_indexes');
// legacy code, return index so states are created
if (empty($states)) {
return $state_index_id;
}
}
// check or synchronize states
if (empty($states)) {
$translations = dbFetchRows('SELECT * FROM `state_translations` WHERE `state_index_id` = ?', array($state_index_id));
if (count($translations) == 0) {
// If we don't have any translations something has gone wrong so return the state_index_id so they get created.
return $state_index_id;
}
} else {
sync_sensor_states($state_index_id, $states);
}
return null;
}
/**
* Synchronize the sensor state translations with the database
*
* @param int $state_index_id index of the state
* @param array $states array of states, each must contain keys: descr, graph, value, generic
*/
function sync_sensor_states($state_index_id, $states)
{
$new_translations = array_reduce($states, function ($array, $state) use ($state_index_id) {
$array[$state['value']] = array(
'state_index_id' => $state_index_id,
'state_descr' => $state['descr'],
'state_draw_graph' => $state['graph'],
'state_value' => $state['value'],
'state_generic_value' => $state['generic']
);
return $array;
}, array());
$existing_translations = dbFetchRows(
'SELECT `state_index_id`,`state_descr`,`state_draw_graph`,`state_value`,`state_generic_value` FROM `state_translations` WHERE `state_index_id`=?',
array($state_index_id)
);
foreach ($existing_translations as $translation) {
$value = $translation['state_value'];
if (isset($new_translations[$value])) {
if ($new_translations[$value] != $translation) {
dbUpdate(
$new_translations[$value],
'state_translations',
'`state_index_id`=? AND `state_value`=?',
array($state_index_id, $value)
);
}
// this translation is synchronized, it doesn't need to be inserted
unset($new_translations[$value]);
} else {
dbDelete('state_translations', '`state_index_id`=? AND `state_value`=?', array($state_index_id, $value));
}
}
// insert any new translations
dbBulkInsert($new_translations, 'state_translations');
}
function create_sensor_to_state_index($device, $state_name, $index)

View File

@@ -25,18 +25,17 @@
namespace LibreNMS\Tests;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;
use LibreNMS\Config;
use PHPUnit_Framework_ExpectationFailedException as PHPUnitException;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
class YamlTest extends \PHPUnit_Framework_TestCase
{
public function testOSYaml()
{
global $config;
$pattern = $config['install_dir'] . '/includes/definitions/*.yaml';
$pattern = Config::get('install_dir') . '/includes/definitions/*.yaml';
foreach (glob($pattern) as $file) {
try {
$data = Yaml::parse(file_get_contents($file));
@@ -50,30 +49,52 @@ class YamlTest extends \PHPUnit_Framework_TestCase
}
}
public function testDiscoveryYaml()
/**
* @dataProvider listDiscoveryFiles
* @param $file
*/
public function testDiscoveryYaml($file)
{
global $config;
try {
$data = Yaml::parse(file_get_contents(Config::get('install_dir') . "/includes/definitions/discovery/$file"));
} catch (ParseException $e) {
throw new PHPUnitException("includes/definitions/discovery/$file Could not be parsed");
}
$pattern = $config['install_dir'] . '/includes/definitions/discovery/*.yaml';
foreach (glob($pattern) as $file) {
try {
$data = Yaml::parse(file_get_contents($file));
} catch (ParseException $e) {
throw new PHPUnitException("$file Could not be parsed");
}
foreach ($data['modules'] as $module => $sub_modules) {
foreach ($sub_modules as $type => $sub_module) {
$this->assertArrayHasKey('data', $sub_module, "$type is missing data key");
foreach ($sub_module['data'] as $sensor_index => $sensor) {
$this->assertArrayHasKey('oid', $sensor, "$type.data.$sensor_index is missing oid key");
if ($type !== 'pre-cache') {
$this->assertArrayHasKey('num_oid', $sensor, "$type.data.$sensor_index(${sensor['oid']}) is missing num_oid key");
$this->assertArrayHasKey('descr', $sensor, "$type.data.$sensor_index(${sensor['oid']}) is missing descr key");
}
foreach ($data['modules'] as $module => $sub_modules) {
foreach ($sub_modules as $type => $sub_module) {
foreach ($sub_module['data'] as $sensor) {
$this->assertArrayHasKey('oid', $sensor, $file);
if ($type !== 'pre-cache') {
$this->assertArrayHasKey('oid', $sensor, $file);
$this->assertArrayHasKey('num_oid', $sensor, $file);
$this->assertArrayHasKey('descr', $sensor, $file);
if ($type === 'state') {
$this->assertArrayHasKey('states', $sensor, "$type.data(${sensor['oid']}) is missing states key");
foreach ($sensor['states'] as $state_index => $state) {
$this->assertArrayHasKey('descr', $state, "$type.data.$sensor_index(${sensor['oid']}).states.$state_index is missing descr key");
$this->assertNotEmpty($state['descr'], "$type.data.$sensor_index(${sensor['oid']}).states.$state_index(${state['descr']}) descr must not be empty");
$this->assertArrayHasKey('graph', $state, "$type.data.$sensor_index(${sensor['oid']}).states.$state_index(${state['descr']}) is missing graph key");
$this->assertTrue($state['graph'] === 0 || $state['graph'] === 1, "$type.data.$sensor_index(${sensor['oid']}).states.$state_index(${state['descr']}) invalid graph value must be 0 or 1");
$this->assertArrayHasKey('value', $state, "$type.data.$sensor_index(${sensor['oid']}).states.$state_index(${state['descr']}) is missing value key");
$this->assertInternalType('int', $state['value'], "$type.data.$sensor_index(${sensor['oid']}).states.$state_index(${state['descr']}) value must be an int");
$this->assertArrayHasKey('generic', $state, "$type.data.$sensor_index(${sensor['oid']}).states.$state_index(${state['descr']}) is missing generic key");
$this->assertInternalType('int', $state['generic'], "$type.data.$sensor_index(${sensor['oid']}).states.$state_index(${state['descr']}) generic must be an int");
}
}
}
}
}
}
public function listDiscoveryFiles()
{
$pattern = Config::get('install_dir') . '/includes/definitions/discovery/*.yaml';
return array_map(function ($file) {
return array(basename($file));
}, glob($pattern));
}
}