diff --git a/doc/Developing/Sensor-State-Support.md b/doc/Developing/Sensor-State-Support.md index c76c55a013..f0041146b2 100644 --- a/doc/Developing/Sensor-State-Support.md +++ b/doc/Developing/Sensor-State-Support.md @@ -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 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); } } ``` diff --git a/includes/dbFacile.php b/includes/dbFacile.php index f2376dd76f..78c03bf784 100644 --- a/includes/dbFacile.php +++ b/includes/dbFacile.php @@ -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) { diff --git a/includes/discovery/functions.inc.php b/includes/discovery/functions.inc.php index 434152492f..4043643220 100644 --- a/includes/discovery/functions.inc.php +++ b/includes/discovery/functions.inc.php @@ -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); } } } diff --git a/includes/functions.php b/includes/functions.php index 6d5a45b486..1f015a44ec 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -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) diff --git a/tests/YamlTest.php b/tests/YamlTest.php index b7fd709d76..3291ef357a 100644 --- a/tests/YamlTest.php +++ b/tests/YamlTest.php @@ -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)); + } }