Issues (2963)

LibreNMS/Device/YamlDiscovery.php (1 issue)

1
<?php
2
/**
3
 * YamlDiscovery.php
4
 *
5
 * -Description-
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 *
20
 * @link       https://www.librenms.org
21
 *
22
 * @copyright  2017 Tony Murray
23
 * @author     Tony Murray <[email protected]>
24
 */
25
26
namespace LibreNMS\Device;
27
28
use Cache;
29
use Illuminate\Support\Arr;
30
use Illuminate\Support\Str;
31
use LibreNMS\Config;
32
use LibreNMS\Exceptions\InvalidOidException;
33
use LibreNMS\Interfaces\Discovery\DiscoveryItem;
34
use LibreNMS\OS;
35
36
class YamlDiscovery
37
{
38
    private static $cache_time = 1800; // 30 min, Used for oid translation cache
39
40
    /**
41
     * @param  OS  $os
42
     * @param  DiscoveryItem|string  $class
43
     * @param  array  $yaml_data
44
     * @return array
45
     */
46
    public static function discover(OS $os, $class, $yaml_data)
47
    {
48
        $pre_cache = $os->preCache();
49
        $device = $os->getDeviceArray();
50
        $items = [];
51
52
        // convert to class name for static call below
53
        if (is_object($class)) {
54
            $class = get_class($class);
55
        }
56
57
        d_echo('YAML Discovery Data: ');
58
        d_echo($yaml_data);
59
60
        foreach ($yaml_data as $first_key => $first_yaml) {
61
            if ($first_key == 'pre-cache') {
62
                continue;
63
            }
64
65
            $group_options = isset($first_yaml['options']) ? $first_yaml['options'] : [];
66
67
            // find the data array, we could already be at for simple modules
68
            if (isset($data['data'])) {
69
                $first_yaml = $first_yaml['data'];
70
            } elseif ($first_key !== 'data') {
71
                continue;
72
            }
73
74
            foreach ($first_yaml as $data) {
75
                $raw_data = (array) $pre_cache[$data['oid']];
76
77
                d_echo("Data {$data['oid']}: ");
78
                d_echo($raw_data);
79
80
                $count = 0;
81
                foreach ($raw_data as $index => $snmp_data) {
82
                    $count++;
83
                    $current_data = [];
84
85
                    // fall back to the fetched oid if value is not specified.  Useful for non-tabular data.
86
                    if (! isset($data['value'])) {
87
                        $data['value'] = $data['oid'];
88
                    }
89
90
                    // determine numeric oid automatically if not specified
91
                    if (! isset($data['num_oid'])) {
92
                        try {
93
                            $data['num_oid'] = static::computeNumericalOID($device, $data);
94
                        } catch (\Exception $e) {
95
                            d_echo('Error: We cannot find a numerical OID for ' . $data['value'] . '. Skipping this one...');
96
                            continue;
97
                        }
98
                    }
99
100
                    foreach ($data as $name => $value) {
101
                        if (in_array($name, ['oid', 'skip_values', 'snmp_flags'])) {
102
                            $current_data[$name] = $value;
103
                        } elseif (Str::contains($value, '{{')) {
104
                            // replace embedded values
105
                            $current_data[$name] = static::replaceValues($name, $index, $count, $data, $pre_cache);
106
                        } else {
107
                            // replace references to data
108
                            $current_data[$name] = static::getValueFromData($name, $index, $data, $pre_cache, $value);
109
                        }
110
                    }
111
112
                    if (static::canSkipItem($current_data['value'], $index, $current_data, $group_options, $snmp_data)) {
113
                        continue;
114
                    }
115
116
                    $item = $class::fromYaml($os, $index, $current_data);
117
118
                    if ($item->isValid()) {
119
                        $items[] = $item;
120
                    }
121
                }
122
            }
123
        }
124
125
        return $items;
126
    }
127
128
    /**
129
     * @param  array  $device  Device we are working on
130
     * @param  array  $data  Array derived from YAML
131
     * @return string
132
     */
133
    public static function computeNumericalOID($device, $data)
134
    {
135
        d_echo('Info: Trying to find a numerical OID for ' . $data['value'] . '.');
136
        $search_mib = $device['dynamic_discovery']['mib'];
137
        $mib_prefix_data_oid = Str::before($data['oid'], '::');
138
        if (! empty($mib_prefix_data_oid) && empty(Str::before($data['value'], '::'))) {
139
            // We should search value in this mib first, as it is explicitely specified
140
            $search_mib = $mib_prefix_data_oid . ':' . $search_mib;
141
        }
142
143
        try {
144
            $num_oid = static::oidToNumeric($data['value'], $device, $search_mib, $device['mib_dir']);
145
        } catch (\Exception $e) {
146
            throw $e;
147
        }
148
149
        d_echo('Info: We found numerical oid for ' . $data['value'] . ': ' . $num_oid);
150
151
        return $num_oid . '.{{ $index }}';
152
    }
153
154
    /**
155
     * @param  string  $name  Name of the field in yaml
156
     * @param  string  $index  index in the snmp table
157
     * @param  int  $count  current count of snmp table entries
158
     * @param  array  $def  yaml definition
159
     * @param  array  $pre_cache  snmp data fetched from device
160
     * @return mixed|string|string[]|null
161
     */
162
    public static function replaceValues($name, $index, $count, $def, $pre_cache)
163
    {
164
        $value = static::getValueFromData($name, $index, $def, $pre_cache);
165
166
        if (is_null($value)) {
167
            // built in replacements
168
            $search = [
169
                '{{ $index }}',
170
                '{{ $count }}',
171
            ];
172
            $replace = [
173
                $index,
174
                $count,
175
            ];
176
177
            // prepare the $subindexX match variable replacement
178
            foreach (explode('.', $index) as $pos => $subindex) {
179
                $search[] = '{{ $subindex' . $pos . ' }}';
180
                $replace[] = $subindex;
181
            }
182
183
            $value = str_replace($search, $replace, $def[$name] ?? '');
184
185
            // search discovery data for values
186
            $value = preg_replace_callback('/{{ \$?([a-zA-Z0-9\-.:]+) }}/', function ($matches) use ($index, $def, $pre_cache) {
187
                $replace = static::getValueFromData($matches[1], $index, $def, $pre_cache, null);
188
                if (is_null($replace)) {
189
                    d_echo('Warning: No variable available to replace ' . $matches[1] . ".\n");
190
191
                    return ''; // remove the unavailable variable
192
                }
193
194
                return $replace;
195
            }, $value);
196
        }
197
198
        return $value;
199
    }
200
201
    /**
202
     * Helper function for dynamic discovery to search for data from pre_cached snmp data
203
     *
204
     * @param  string  $name  The name of the field from the discovery data or just an oid
205
     * @param  string|int  $index  The index of the current sensor
206
     * @param  array  $discovery_data  The discovery data for the current sensor
207
     * @param  array  $pre_cache  all pre-cached snmp data
208
     * @param  mixed  $default  The default value to return if data is not found
209
     * @return mixed
210
     */
211
    public static function getValueFromData($name, $index, $discovery_data, $pre_cache, $default = null)
212
    {
213
        if (isset($discovery_data[$name])) {
214
            $name = $discovery_data[$name];
215
        }
216
217
        if (! is_array($discovery_data['oid']) && isset($pre_cache[$discovery_data['oid']][$index]) && isset($pre_cache[$discovery_data['oid']][$index][$name])) {
218
            return $pre_cache[$discovery_data['oid']][$index][$name];
219
        }
220
221
        if (isset($pre_cache[$index][$name])) {
222
            return $pre_cache[$index][$name];
223
        }
224
225
        //create the sub-index values in order to try to match them with precache
226
        $sub_indexes = explode('.', $index);
227
        // parse sub_index options name with trailing colon and index
228
        $sub_index = 0;
229
        $sub_index_end = null;
230
        if (preg_match('/^(.+):(\d+)(?:-(\d+))?$/', $name, $matches)) {
231
            [,$name, $sub_index, $sub_index_end] = $matches;
232
        }
233
234
        if (isset($pre_cache[$name]) && ! is_numeric($name)) {
235
            if (is_array($pre_cache[$name])) {
236
                if (isset($pre_cache[$name][$index][$name])) {
237
                    return $pre_cache[$name][$index][$name];
238
                } elseif (isset($pre_cache[$name][$index])) {
239
                    return $pre_cache[$name][$index];
240
                } elseif (count($pre_cache[$name]) === 1 && ! is_array(current($pre_cache[$name]))) {
241
                    return current($pre_cache[$name]);
242
                } elseif (isset($sub_indexes[$sub_index])) {
243
                    if ($sub_index_end) {
244
                        $multi_sub_index = implode('.', array_slice($sub_indexes, $sub_index, $sub_index_end));
245
                        if (isset($pre_cache[$name][$multi_sub_index][$name])) {
246
                            return $pre_cache[$name][$multi_sub_index][$name];
247
                        }
248
                    }
249
250
                    if (isset($pre_cache[$name][$sub_indexes[$sub_index]][$name])) {
251
                        return $pre_cache[$name][$sub_indexes[$sub_index]][$name];
252
                    }
253
                }
254
            } else {
255
                return $pre_cache[$name];
256
            }
257
        }
258
259
        return $default;
260
    }
261
262
    public static function preCache(OS $os)
263
    {
264
        // Pre-cache data for later use
265
        $pre_cache = [];
266
        $device = $os->getDeviceArray();
267
268
        $pre_cache_file = 'includes/discovery/sensors/pre-cache/' . $device['os'] . '.inc.php';
269
        if (is_file($pre_cache_file)) {
270
            echo "Pre-cache {$device['os']}: ";
271
            include $pre_cache_file;
272
            echo PHP_EOL;
273
            d_echo($pre_cache);
274
        }
275
276
        // TODO change to exclude os with pre-cache php file, but just exclude them by hand for now (like avtech)
277
        if ($device['os'] == 'avtech') {
278
            return $pre_cache;
279
        }
280
281
        if (! empty($device['dynamic_discovery']['modules'])) {
282
            echo 'Caching data: ';
283
            foreach ($device['dynamic_discovery']['modules'] as $module => $discovery_data) {
284
                echo "$module ";
285
                foreach ($discovery_data as $key => $data_array) {
286
                    // find the data array, we could already be at for simple modules
287
                    if (isset($data_array['data'])) {
288
                        $data_array = $data_array['data'];
289
                    } elseif ($key !== 'data') {
290
                        continue;
291
                    }
292
293
                    $saved_nobulk = Config::getOsSetting($os->getName(), 'snmp_bulk', true);
294
295
                    foreach ($data_array as $data) {
296
                        foreach ((array) $data['oid'] as $oid) {
297
                            if (! array_key_exists($oid, $pre_cache)) {
298
                                if (isset($data['snmp_flags'])) {
299
                                    $snmp_flag = Arr::wrap($data['snmp_flags']);
300
                                } elseif (Str::contains($oid, '::')) {
301
                                    $snmp_flag = ['-OteQUS'];
302
                                } else {
303
                                    $snmp_flag = ['-OteQUs'];
304
                                }
305
                                $snmp_flag[] = '-Ih';
306
307
                                // disable bulk request for specific data
308
                                if (isset($data['snmp_bulk'])) {
309
                                    Config::set('os.' . $os->getName() . '.snmp_bulk', (bool) $data['snmp_bulk']);
310
                                }
311
312
                                $mib = $device['dynamic_discovery']['mib'];
313
                                $pre_cache[$oid] = snmpwalk_cache_oid($device, $oid, $pre_cache[$oid] ?? [], $mib, null, $snmp_flag);
314
315
                                Config::set('os.' . $os->getName() . '.snmp_bulk', $saved_nobulk);
316
                            }
317
                        }
318
                    }
319
                }
320
            }
321
            echo PHP_EOL;
322
        }
323
324
        return $pre_cache;
325
    }
326
327
    /**
328
     * Check to see if we should skip this discovery item
329
     *
330
     * @param  mixed  $value
331
     * @param  array  $yaml_item_data  The data key from this item
332
     * @param  array  $group_options  The options key from this group of items
333
     * @param  array  $pre_cache  The pre-cache data array
334
     * @return bool
335
     */
336
    public static function canSkipItem($value, $index, $yaml_item_data, $group_options, $pre_cache = [])
337
    {
338
        $skip_values = array_replace((array) ($group_options['skip_values'] ?? []), (array) ($yaml_item_data['skip_values'] ?? []));
339
340
        foreach ($skip_values as $skip_value) {
341
            if (is_array($skip_value) && $pre_cache) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pre_cache of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
342
                // Dynamic skipping of data
343
                $op = $skip_value['op'] ?? '!=';
344
                $tmp_value = static::getValueFromData($skip_value['oid'], $index, $yaml_item_data, $pre_cache);
345
                if (Str::contains($skip_value['oid'], '.')) {
346
                    [$skip_value['oid'], $targeted_index] = explode('.', $skip_value['oid'], 2);
347
                    $tmp_value = static::getValueFromData($skip_value['oid'], $targeted_index, $yaml_item_data, $pre_cache);
348
                }
349
                if (compare_var($tmp_value, $skip_value['value'], $op)) {
350
                    return true;
351
                }
352
            }
353
            if ($value == $skip_value) {
354
                return true;
355
            }
356
        }
357
358
        $skip_value_lt = array_replace((array) ($group_options['skip_value_lt'] ?? []), (array) ($yaml_item_data['skip_value_lt'] ?? []));
359
        foreach ($skip_value_lt as $skip_value) {
360
            if ($value < $skip_value) {
361
                return true;
362
            }
363
        }
364
365
        $skip_value_gt = array_replace((array) ($group_options['skip_value_gt'] ?? []), (array) ($yaml_item_data['skip_value_gt'] ?? []));
366
        foreach ($skip_value_gt as $skip_value) {
367
            if ($value > $skip_value) {
368
                return true;
369
            }
370
        }
371
372
        return false;
373
    }
374
375
    /**
376
     * Translate an oid to numeric format (if already numeric, return as-is)
377
     *
378
     * @param  string  $oid
379
     * @param  array|null  $device
380
     * @param  string  $mib
381
     * @param  string|null  $mibdir
382
     * @return string numeric oid
383
     *
384
     * @throws \LibreNMS\Exceptions\InvalidOidException
385
     */
386
    public static function oidToNumeric($oid, $device = null, $mib = 'ALL', $mibdir = null)
387
    {
388
        if (self::oidIsNumeric($oid)) {
389
            return $oid;
390
        }
391
        $key = 'YamlDiscovery:oidToNumeric:' . $mibdir . '/' . $mib . '/' . $oid;
392
        if (Cache::has($key)) {
393
            $numeric_oid = Cache::get($key);
394
        } else {
395
            foreach (explode(':', $mib) as $mib_name) {
396
                $numeric_oid = snmp_translate($oid, $mib_name, $mibdir, null, $device);
397
                if ($numeric_oid) {
398
                    break;
399
                }
400
            }
401
        }
402
403
        //Store the value
404
        Cache::put($key, $numeric_oid, self::$cache_time);
405
406
        if (empty($numeric_oid)) {
407
            throw new InvalidOidException("Unable to translate oid $oid");
408
        }
409
410
        return $numeric_oid;
411
    }
412
413
    public static function oidIsNumeric($oid)
414
    {
415
        return (bool) preg_match('/^[.\d]+$/', $oid);
416
    }
417
}
418