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
|
|||
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 |
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.