Issues (2963)

includes/polling/functions.inc.php (1 issue)

1
<?php
2
3
use App\Models\DeviceGraph;
4
use Illuminate\Support\Str;
5
use LibreNMS\Config;
6
use LibreNMS\Enum\Alert;
7
use LibreNMS\Exceptions\JsonAppBlankJsonException;
8
use LibreNMS\Exceptions\JsonAppExtendErroredException;
9
use LibreNMS\Exceptions\JsonAppMissingKeysException;
10
use LibreNMS\Exceptions\JsonAppParsingFailedException;
11
use LibreNMS\Exceptions\JsonAppPollingFailedException;
12
use LibreNMS\Exceptions\JsonAppWrongVersionException;
13
use LibreNMS\RRD\RrdDefinition;
14
15
function bulk_sensor_snmpget($device, $sensors)
16
{
17
    $oid_per_pdu = get_device_oid_limit($device);
18
    $sensors = array_chunk($sensors, $oid_per_pdu);
0 ignored issues
show
It seems like $oid_per_pdu can also be of type null; however, parameter $length of array_chunk() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

18
    $sensors = array_chunk($sensors, /** @scrutinizer ignore-type */ $oid_per_pdu);
Loading history...
19
    $cache = [];
20
    foreach ($sensors as $chunk) {
21
        $oids = array_map(function ($data) {
22
            return $data['sensor_oid'];
23
        }, $chunk);
24
        $oids = implode(' ', $oids);
25
        $multi_response = snmp_get_multi_oid($device, $oids, '-OUQnte');
26
        $cache = array_merge($cache, $multi_response);
27
    }
28
29
    return $cache;
30
}
31
32
/**
33
 * @param $device
34
 * @param  string  $type  type/class of sensor
35
 * @return array
36
 */
37
function sensor_precache($device, $type)
38
{
39
    $sensor_cache = [];
40
    if (file_exists('includes/polling/sensors/pre-cache/' . $device['os'] . '.inc.php')) {
41
        include 'includes/polling/sensors/pre-cache/' . $device['os'] . '.inc.php';
42
    }
43
44
    return $sensor_cache;
45
}
46
47
function poll_sensor($device, $class)
48
{
49
    global $agent_sensors;
50
51
    $sensors = [];
52
    $misc_sensors = [];
53
    $all_sensors = [];
54
55
    foreach (dbFetchRows('SELECT * FROM `sensors` WHERE `sensor_class` = ? AND `device_id` = ?', [$class, $device['device_id']]) as $sensor) {
56
        if ($sensor['poller_type'] == 'agent') {
57
            // Agent sensors are polled in the unix-agent
58
        } elseif ($sensor['poller_type'] == 'ipmi') {
59
            $misc_sensors[] = $sensor;
60
        } else {
61
            $sensors[] = $sensor;
62
        }
63
    }
64
65
    $snmp_data = bulk_sensor_snmpget($device, $sensors);
66
67
    $sensor_cache = sensor_precache($device, $class);
68
69
    foreach ($sensors as $sensor) {
70
        echo 'Checking (' . $sensor['poller_type'] . ") $class " . $sensor['sensor_descr'] . '... ' . PHP_EOL;
71
72
        if ($sensor['poller_type'] == 'snmp') {
73
            $mibdir = null;
74
75
            $sensor_value = trim(str_replace('"', '', $snmp_data[$sensor['sensor_oid']]));
76
77
            if (file_exists('includes/polling/sensors/' . $class . '/' . $device['os'] . '.inc.php')) {
78
                require 'includes/polling/sensors/' . $class . '/' . $device['os'] . '.inc.php';
79
            } elseif (file_exists('includes/polling/sensors/' . $class . '/' . $device['os_group'] . '.inc.php')) {
80
                require 'includes/polling/sensors/' . $class . '/' . $device['os_group'] . '.inc.php';
81
            }
82
83
            if ($class == 'temperature') {
84
                preg_match('/[\d\.\-]+/', $sensor_value, $temp_response);
85
                if (! empty($temp_response[0])) {
86
                    $sensor_value = $temp_response[0];
87
                }
88
            } elseif ($class == 'state') {
89
                if (! is_numeric($sensor_value)) {
90
                    $state_value = dbFetchCell(
91
                        'SELECT `state_value`
92
                        FROM `state_translations` LEFT JOIN `sensors_to_state_indexes`
93
                        ON `state_translations`.`state_index_id` = `sensors_to_state_indexes`.`state_index_id`
94
                        WHERE `sensors_to_state_indexes`.`sensor_id` = ?
95
                        AND `state_translations`.`state_descr` LIKE ?',
96
                        [$sensor['sensor_id'], $sensor_value]
97
                    );
98
                    d_echo('State value of ' . $sensor_value . ' is ' . $state_value . "\n");
99
                    if (is_numeric($state_value)) {
100
                        $sensor_value = $state_value;
101
                    }
102
                }
103
            }//end if
104
            unset($mib);
105
            unset($mibdir);
106
            $sensor['new_value'] = $sensor_value;
107
            $all_sensors[] = $sensor;
108
        }
109
    }
110
111
    foreach ($misc_sensors as $sensor) {
112
        if ($sensor['poller_type'] == 'agent') {
113
            if (isset($agent_sensors)) {
114
                $sensor_value = $agent_sensors[$class][$sensor['sensor_type']][$sensor['sensor_index']]['current'];
115
                $sensor['new_value'] = $sensor_value;
116
                $all_sensors[] = $sensor;
117
            } else {
118
                echo "no agent data!\n";
119
                continue;
120
            }
121
        } elseif ($sensor['poller_type'] == 'ipmi') {
122
            echo " already polled.\n";
123
            // ipmi should probably move here from the ipmi poller file (FIXME)
124
            continue;
125
        } else {
126
            echo "unknown poller type!\n";
127
            continue;
128
        }//end if
129
    }
130
    record_sensor_data($device, $all_sensors);
131
}//end poll_sensor()
132
133
/**
134
 * @param $device
135
 * @param $all_sensors
136
 */
137
function record_sensor_data($device, $all_sensors)
138
{
139
    $supported_sensors = [
140
        'current'     => 'A',
141
        'frequency'   => 'Hz',
142
        'runtime'     => 'Min',
143
        'humidity'    => '%',
144
        'fanspeed'    => 'rpm',
145
        'power'       => 'W',
146
        'voltage'     => 'V',
147
        'temperature' => 'C',
148
        'dbm'         => 'dBm',
149
        'charge'      => '%',
150
        'load'        => '%',
151
        'state'       => '#',
152
        'signal'      => 'dBm',
153
        'airflow'     => 'cfm',
154
        'snr'         => 'SNR',
155
        'pressure'    => 'kPa',
156
        'cooling'     => 'W',
157
    ];
158
159
    foreach ($all_sensors as $sensor) {
160
        $class = ucfirst($sensor['sensor_class']);
161
        $unit = $supported_sensors[$sensor['sensor_class']];
162
        $sensor_value = cast_number($sensor['new_value']);
163
        $prev_sensor_value = $sensor['sensor_current'];
164
165
        if ($sensor_value == -32768 || is_nan($sensor_value)) {
166
            echo 'Invalid (-32768 or NaN)';
167
            $sensor_value = 0;
168
        }
169
170
        if ($sensor['sensor_divisor'] && $sensor_value !== 0) {
171
            $sensor_value = ($sensor_value / $sensor['sensor_divisor']);
172
        }
173
174
        if ($sensor['sensor_multiplier']) {
175
            $sensor_value = ($sensor_value * $sensor['sensor_multiplier']);
176
        }
177
178
        if (isset($sensor['user_func']) && is_callable($sensor['user_func'])) {
179
            $sensor_value = $sensor['user_func']($sensor_value);
180
        }
181
182
        $rrd_name = get_sensor_rrd_name($device, $sensor);
183
184
        $rrd_def = RrdDefinition::make()->addDataset('sensor', 'GAUGE');
185
186
        echo "$sensor_value $unit\n";
187
188
        $fields = [
189
            'sensor' => $sensor_value,
190
        ];
191
192
        $tags = [
193
            'sensor_class' => $sensor['sensor_class'],
194
            'sensor_type' => $sensor['sensor_type'],
195
            'sensor_descr' => $sensor['sensor_descr'],
196
            'sensor_index' => $sensor['sensor_index'],
197
            'rrd_name' => $rrd_name,
198
            'rrd_def' => $rrd_def,
199
        ];
200
        data_update($device, 'sensor', $tags, $fields);
201
202
        // FIXME also warn when crossing WARN level!
203
        if ($sensor['sensor_limit_low'] != '' && $prev_sensor_value > $sensor['sensor_limit_low'] && $sensor_value < $sensor['sensor_limit_low'] && $sensor['sensor_alert'] == 1) {
204
            echo 'Alerting for ' . $device['hostname'] . ' ' . $sensor['sensor_descr'] . "\n";
205
            log_event("$class under threshold: $sensor_value $unit (< {$sensor['sensor_limit_low']} $unit)", $device, $sensor['sensor_class'], 4, $sensor['sensor_id']);
206
        } elseif ($sensor['sensor_limit'] != '' && $prev_sensor_value < $sensor['sensor_limit'] && $sensor_value > $sensor['sensor_limit'] && $sensor['sensor_alert'] == 1) {
207
            echo 'Alerting for ' . $device['hostname'] . ' ' . $sensor['sensor_descr'] . "\n";
208
            log_event("$class above threshold: $sensor_value $unit (> {$sensor['sensor_limit']} $unit)", $device, $sensor['sensor_class'], 4, $sensor['sensor_id']);
209
        }
210
        if ($sensor['sensor_class'] == 'state' && $prev_sensor_value != $sensor_value) {
211
            $trans = array_column(
212
                dbFetchRows(
213
                    'SELECT `state_translations`.`state_value`, `state_translations`.`state_descr` FROM `sensors_to_state_indexes` LEFT JOIN `state_translations` USING (`state_index_id`) WHERE `sensors_to_state_indexes`.`sensor_id`=? AND `state_translations`.`state_value` IN (?,?)',
214
                    [$sensor['sensor_id'], $sensor_value, $prev_sensor_value]
215
                ),
216
                'state_descr',
217
                'state_value'
218
            );
219
220
            log_event("$class sensor {$sensor['sensor_descr']} has changed from {$trans[$prev_sensor_value]} ($prev_sensor_value) to {$trans[$sensor_value]} ($sensor_value)", $device, $class, 3, $sensor['sensor_id']);
221
        }
222
        if ($sensor_value != $prev_sensor_value) {
223
            dbUpdate(['sensor_current' => $sensor_value, 'sensor_prev' => $prev_sensor_value, 'lastupdate' => ['NOW()']], 'sensors', '`sensor_class` = ? AND `sensor_id` = ?', [$sensor['sensor_class'], $sensor['sensor_id']]);
224
        }
225
    }
226
}
227
228
/**
229
 * @param  array  $device  The device to poll
230
 * @param  bool  $force_module  Ignore device module overrides
231
 * @return bool
232
 */
233
function poll_device($device, $force_module = false)
234
{
235
    global $device, $graphs;
236
237
    $device_start = microtime(true);
238
239
    $attribs = DeviceCache::getPrimary()->getAttribs();
240
    $device['attribs'] = $attribs;
241
242
    load_os($device);
243
    $os = \LibreNMS\OS::make($device);
244
245
    unset($array);
246
247
    // Start counting device poll time
248
    echo 'Hostname:    ' . $device['hostname'] . PHP_EOL;
249
    echo 'Device ID:   ' . $device['device_id'] . PHP_EOL;
250
    echo 'OS:          ' . $device['os'] . PHP_EOL;
251
252
    if (empty($device['overwrite_ip'])) {
253
        $ip = dnslookup($device);
254
    } else {
255
        $ip = $device['overwrite_ip'];
256
    }
257
258
    $db_ip = null;
259
    if (! empty($ip)) {
260
        if (empty($device['overwrite_ip'])) {
261
            echo 'Resolved IP: ' . $ip . PHP_EOL;
262
        } else {
263
            echo 'Assigned IP: ' . $ip . PHP_EOL;
264
        }
265
        $db_ip = inet_pton($ip);
266
    }
267
268
    if (! empty($db_ip) && inet6_ntop($db_ip) != inet6_ntop($device['ip'])) {
269
        log_event('Device IP changed to ' . $ip, $device, 'system', 3);
270
        dbUpdate(['ip' => $db_ip], 'devices', 'device_id=?', [$device['device_id']]);
271
    }
272
273
    if ($os_group = Config::get("os.{$device['os']}.group")) {
274
        $device['os_group'] = $os_group;
275
        echo ' (' . $device['os_group'] . ')';
276
    }
277
278
    echo PHP_EOL . PHP_EOL;
279
280
    unset($poll_update);
281
    unset($poll_update_query);
282
    unset($poll_separator);
283
    $poll_update_array = [];
284
    $update_array = [];
285
286
    $host_rrd = Rrd::name($device['hostname'], '', '');
287
    if (Config::get('norrd') !== true && ! is_dir($host_rrd)) {
288
        mkdir($host_rrd);
289
        echo "Created directory : $host_rrd\n";
290
    }
291
292
    $response = device_is_up($device, true);
293
294
    if ($response['status'] == '1') {
295
        if ($device['snmp_disable']) {
296
            Config::set('poller_modules', ['availability' => true]);
297
        } else {
298
            // we always want the core module to be included, prepend it
299
            Config::set('poller_modules', ['core' => true, 'availability' => true] + Config::get('poller_modules'));
300
        }
301
302
        printChangedStats(true); // don't count previous stats
303
        foreach (Config::get('poller_modules') as $module => $module_status) {
304
            $os_module_status = Config::get("os.{$device['os']}.poller_modules.$module");
305
            d_echo('Modules status: Global' . (isset($module_status) ? ($module_status ? '+ ' : '- ') : '  '));
306
            d_echo('OS' . (isset($os_module_status) ? ($os_module_status ? '+ ' : '- ') : '  '));
307
            d_echo('Device' . (isset($attribs['poll_' . $module]) ? ($attribs['poll_' . $module] ? '+ ' : '- ') : '  '));
308
            if ($force_module === true ||
309
                $attribs['poll_' . $module] ||
310
                ($os_module_status && ! isset($attribs['poll_' . $module])) ||
311
                ($module_status && ! isset($os_module_status) && ! isset($attribs['poll_' . $module]))) {
312
                $start_memory = memory_get_usage();
313
                $module_start = microtime(true);
314
                echo "\n#### Load poller module $module ####\n";
315
316
                try {
317
                    include "includes/polling/$module.inc.php";
318
                } catch (Exception $e) {
319
                    // isolate module exceptions so they don't disrupt the polling process
320
                    echo $e->getTraceAsString() . PHP_EOL;
321
                    c_echo("%rError in $module module.%n " . $e->getMessage() . PHP_EOL);
322
                    logfile("Error in $module module. " . $e->getMessage() . PHP_EOL . $e->getTraceAsString() . PHP_EOL);
323
                }
324
325
                $module_time = microtime(true) - $module_start;
326
                $module_mem = (memory_get_usage() - $start_memory);
327
                printf("\n>> Runtime for poller module '%s': %.4f seconds with %s bytes\n", $module, $module_time, $module_mem);
328
                printChangedStats();
329
                echo "#### Unload poller module $module ####\n\n";
330
331
                // save per-module poller stats
332
                $tags = [
333
                    'module'      => $module,
334
                    'rrd_def'     => RrdDefinition::make()->addDataset('poller', 'GAUGE', 0),
335
                    'rrd_name'    => ['poller-perf', $module],
336
                ];
337
                $fields = [
338
                    'poller' => $module_time,
339
                ];
340
                data_update($device, 'poller-perf', $tags, $fields);
341
                $os->enableGraph('poller_perf');
342
343
                // remove old rrd
344
                $oldrrd = Rrd::name($device['hostname'], ['poller', $module, 'perf']);
345
                if (is_file($oldrrd)) {
346
                    unlink($oldrrd);
347
                }
348
                unset($tags, $fields, $oldrrd);
349
            } elseif (isset($attribs['poll_' . $module]) && $attribs['poll_' . $module] == '0') {
350
                echo "Module [ $module ] disabled on host.\n\n";
351
            } elseif (isset($os_module_status) && $os_module_status == '0') {
352
                echo "Module [ $module ] disabled on os.\n\n";
353
            } else {
354
                echo "Module [ $module ] disabled globally.\n\n";
355
            }
356
        }
357
358
        // Ping response
359
        if (can_ping_device($attribs) === true && ! empty($response['ping_time'])) {
360
            $tags = [
361
                'rrd_def' => RrdDefinition::make()->addDataset('ping', 'GAUGE', 0, 65535),
362
            ];
363
            $fields = [
364
                'ping' => $response['ping_time'],
365
            ];
366
367
            $update_array['last_ping'] = ['NOW()'];
368
            $update_array['last_ping_timetaken'] = $response['ping_time'];
369
370
            data_update($device, 'ping-perf', $tags, $fields);
371
            $os->enableGraph('ping_perf');
372
        }
373
374
        $device_time = round(microtime(true) - $device_start, 3);
375
376
        // Poller performance
377
        if (! empty($device_time)) {
378
            $tags = [
379
                'rrd_def' => RrdDefinition::make()->addDataset('poller', 'GAUGE', 0),
380
                'module'  => 'ALL',
381
            ];
382
            $fields = [
383
                'poller' => $device_time,
384
            ];
385
386
            data_update($device, 'poller-perf', $tags, $fields);
387
            $os->enableGraph('poller_modules_perf');
388
        }
389
390
        if (! $force_module) {
391
            // don't update last_polled time if we are forcing a specific module to be polled
392
            $update_array['last_polled'] = ['NOW()'];
393
            $update_array['last_polled_timetaken'] = $device_time;
394
395
            echo 'Enabling graphs: ';
396
            DeviceGraph::deleted(function ($graph) {
397
                echo '-';
398
            });
399
            DeviceGraph::created(function ($graph) {
400
                echo '+';
401
            });
402
403
            $os->persistGraphs();
404
            echo PHP_EOL;
405
        }
406
407
        $updated = dbUpdate($update_array, 'devices', '`device_id` = ?', [$device['device_id']]);
408
        if ($updated) {
409
            d_echo('Updating ' . $device['hostname'] . PHP_EOL);
410
        }
411
412
        echo "\nPolled in $device_time seconds\n";
413
414
        // check if the poll took to long and log an event
415
        if ($device_time > Config::get('rrd.step')) {
416
            log_event('Polling took longer than ' . round(Config::get('rrd.step') / 60, 2) .
417
                ' minutes!  This will cause gaps in graphs.', $device, 'system', 5);
418
        }
419
420
        unset($storage_cache);
421
        // Clear cache of hrStorage ** MAYBE FIXME? **
422
        unset($cache);
423
        // Clear cache (unify all things here?)
424
425
        return true; // device was polled
426
    }
427
428
    return false; // device not polled
429
}//end poll_device()
430
431
/**
432
 * Update the application status and output in the database.
433
 *
434
 * Metric values should have key for of the matching name.
435
 * If you have multiple groups of metrics, you can group them with multiple sub arrays
436
 * The group name (key) will be prepended to each metric in that group, separated by an underscore
437
 * The special group "none" will not be prefixed.
438
 *
439
 * @param  array  $app  app from the db, including app_id
440
 * @param  string  $response  This should be the return state of Application polling
441
 * @param  array  $metrics  an array of additional metrics to store in the database for alerting
442
 * @param  string  $status  This is the current value for alerting
443
 */
444
function update_application($app, $response, $metrics = [], $status = '')
445
{
446
    if (! is_numeric($app['app_id'])) {
447
        d_echo('$app does not contain app_id, could not update');
448
449
        return;
450
    }
451
452
    $data = [
453
        'app_state'  => 'UNKNOWN',
454
        'app_status' => $status,
455
        'timestamp'  => ['NOW()'],
456
    ];
457
458
    if ($response != '' && $response !== false) {
459
        if (Str::contains($response, [
460
            'Traceback (most recent call last):',
461
        ])) {
462
            $data['app_state'] = 'ERROR';
463
        } elseif (in_array($response, ['OK', 'ERROR', 'LEGACY', 'UNSUPPORTED'])) {
464
            $data['app_state'] = $response;
465
        } else {
466
            // should maybe be 'unknown' as state
467
            $data['app_state'] = 'OK';
468
        }
469
    }
470
471
    if ($data['app_state'] != $app['app_state']) {
472
        $data['app_state_prev'] = $app['app_state'];
473
474
        $device = dbFetchRow('SELECT * FROM devices LEFT JOIN applications ON devices.device_id=applications.device_id WHERE applications.app_id=?', [$app['app_id']]);
475
476
        $app_name = \LibreNMS\Util\StringHelpers::nicecase($app['app_type']);
477
478
        switch ($data['app_state']) {
479
            case 'OK':
480
                $severity = Alert::OK;
481
                $event_msg = 'changed to OK';
482
                break;
483
            case 'ERROR':
484
                $severity = Alert::ERROR;
485
                $event_msg = 'ends with ERROR';
486
                break;
487
            case 'LEGACY':
488
                $severity = Alert::WARNING;
489
                $event_msg = 'Client Agent is deprecated';
490
                break;
491
            case 'UNSUPPORTED':
492
                $severity = Alert::ERROR;
493
                $event_msg = 'Client Agent Version is not supported';
494
                break;
495
            default:
496
                $severity = Alert::UNKNOWN;
497
                $event_msg = 'has UNKNOWN state';
498
                break;
499
        }
500
        log_event('Application ' . $app_name . ' ' . $event_msg, $device, 'application', $severity);
501
    }
502
    dbUpdate($data, 'applications', '`app_id` = ?', [$app['app_id']]);
503
504
    // update metrics
505
    if (! empty($metrics)) {
506
        $db_metrics = dbFetchRows('SELECT * FROM `application_metrics` WHERE app_id=?', [$app['app_id']]);
507
        $db_metrics = array_by_column($db_metrics, 'metric');
508
509
        // allow two level metrics arrays, flatten them and prepend the group name
510
        if (is_array(current($metrics))) {
511
            $metrics = array_reduce(
512
                array_keys($metrics),
513
                function ($carry, $metric_group) use ($metrics) {
514
                    if ($metric_group == 'none') {
515
                        $prefix = '';
516
                    } else {
517
                        $prefix = $metric_group . '_';
518
                    }
519
520
                    foreach ($metrics[$metric_group] as $metric_name => $value) {
521
                        $carry[$prefix . $metric_name] = $value;
522
                    }
523
524
                    return $carry;
525
                },
526
                []
527
            );
528
        }
529
530
        echo ': ';
531
        foreach ($metrics as $metric_name => $value) {
532
            if (! isset($db_metrics[$metric_name])) {
533
                // insert new metric
534
                dbInsert(
535
                    [
536
                        'app_id' => $app['app_id'],
537
                        'metric' => $metric_name,
538
                        'value' => $value,
539
                    ],
540
                    'application_metrics'
541
                );
542
                echo '+';
543
            } elseif ($value != $db_metrics[$metric_name]['value']) {
544
                dbUpdate(
545
                    [
546
                        'value' => $value,
547
                        'value_prev' => $db_metrics[$metric_name]['value'],
548
                    ],
549
                    'application_metrics',
550
                    'app_id=? && metric=?',
551
                    [$app['app_id'], $metric_name]
552
                );
553
                echo 'U';
554
            } else {
555
                echo '.';
556
            }
557
558
            unset($db_metrics[$metric_name]);
559
        }
560
561
        // remove no longer existing metrics (generally should not happen
562
        foreach ($db_metrics as $db_metric) {
563
            dbDelete(
564
                'application_metrics',
565
                'app_id=? && metric=?',
566
                [$app['app_id'], $db_metric['metric']]
567
            );
568
            echo '-';
569
        }
570
571
        echo PHP_EOL;
572
    }
573
}
574
575
/**
576
 * This is to make it easier polling apps. Also to help standardize around JSON.
577
 *
578
 * The required keys for the returned JSON are as below.
579
 *  version     - The version of the snmp extend script. Should be numeric and at least 1.
580
 *  error       - Error code from the snmp extend script. Should be > 0 (0 will be ignored and negatives are reserved)
581
 *  errorString - Text to describe the error.
582
 *  data        - An key with an array with the data to be used.
583
 *
584
 * If the app returns an error, an exception will be raised.
585
 * Positive numbers will be errors returned by the extend script.
586
 *
587
 * Possible parsing related errors:
588
 * -2 : Failed to fetch data from the device
589
 * -3 : Could not decode the JSON.
590
 * -4 : Empty JSON parsed, meaning blank JSON was returned.
591
 * -5 : Valid json, but missing required keys
592
 * -6 : Returned version is less than the min version.
593
 *
594
 * Error checking may also be done via checking the exceptions listed below.
595
 *   JsonAppPollingFailedException, -2 : Empty return from SNMP.
596
 *   JsonAppParsingFailedException, -3 : Could not parse the JSON.
597
 *   JsonAppBlankJsonException, -4     : Blank JSON.
598
 *   JsonAppMissingKeysException, -5   : Missing required keys.
599
 *   JsonAppWrongVersionException , -6 : Older version than supported.
600
 *   JsonAppExtendErroredException     : Polling and parsing was good, but the returned data has an error set.
601
 *                                       This may be checked via $e->getParsedJson() and then checking the
602
 *                                       keys error and errorString.
603
 * The error value can be accessed via $e->getCode()
604
 * The output can be accessed via $->getOutput() Only returned for code -3 or lower.
605
 * The parsed JSON can be access via $e->getParsedJson()
606
 *
607
 * All of the exceptions extend JsonAppException.
608
 *
609
 * If the error is less than -1, you can assume it is a legacy snmp extend script.
610
 *
611
 * @param  array  $device
612
 * @param  string  $extend  the extend name. For example, if 'zfs' is passed it will be converted to 'nsExtendOutputFull.3.122.102.115'.
613
 * @param  int  $min_version  the minimum version to accept for the returned JSON. default: 1
614
 * @return array The json output data parsed into an array
615
 *
616
 * @throws JsonAppBlankJsonException
617
 * @throws JsonAppExtendErroredException
618
 * @throws JsonAppMissingKeysException
619
 * @throws JsonAppParsingFailedException
620
 * @throws JsonAppPollingFailedException
621
 * @throws JsonAppWrongVersionException
622
 */
623
function json_app_get($device, $extend, $min_version = 1)
624
{
625
    $output = snmp_get($device, 'nsExtendOutputFull.' . string_to_oid($extend), '-Oqv', 'NET-SNMP-EXTEND-MIB');
626
627
    // make sure we actually get something back
628
    if (empty($output)) {
629
        throw new JsonAppPollingFailedException('Empty return from snmp_get.', -2);
630
    }
631
632
    //  turn the JSON into a array
633
    $parsed_json = json_decode(stripslashes($output), true);
634
635
    // improper JSON or something else was returned. Populate the variable with an error.
636
    if (json_last_error() !== JSON_ERROR_NONE) {
637
        throw new JsonAppParsingFailedException('Invalid JSON', $output, -3);
638
    }
639
640
    // There no keys in the array, meaning '{}' was was returned
641
    if (empty($parsed_json)) {
642
        throw new JsonAppBlankJsonException('Blank JSON returned.', $output, -4);
643
    }
644
645
    // It is a legacy JSON app extend, meaning these are not set
646
    if (! isset($parsed_json['error'], $parsed_json['data'], $parsed_json['errorString'], $parsed_json['version'])) {
647
        throw new JsonAppMissingKeysException('Legacy script or extend error, missing one or more required keys.', $output, $parsed_json, -5);
648
    }
649
650
    if ($parsed_json['version'] < $min_version) {
651
        throw new JsonAppWrongVersionException("Script,'" . $parsed_json['version'] . "', older than required version of '$min_version'", $output, $parsed_json, -6);
652
    }
653
654
    if ($parsed_json['error'] != 0) {
655
        throw new JsonAppExtendErroredException("Script returned exception: {$parsed_json['errorString']}", $output, $parsed_json, $parsed_json['error']);
656
    }
657
658
    return $parsed_json;
659
}
660
661
/**
662
 * Some data arrays returned with json_app_get are deeper than
663
 * update_application likes. This recurses through the array
664
 * and flattens it out so it can nicely be inserted into the
665
 * database.
666
 *
667
 * One argument is taken and that is the array to flatten.
668
 *
669
 * @param  array  $array
670
 * @param  string  $prefix  What to prefix to the name. Defaults to '', nothing.
671
 * @param  string  $joiner  The string to join the prefix, if set to something other
672
 *                          than '', and array keys with.
673
 * @return array The flattened array.
674
 */
675
function data_flatten($array, $prefix = '', $joiner = '_')
676
{
677
    $return = [];
678
    foreach ($array as $key => $value) {
679
        if (is_array($value)) {
680
            if (strcmp($prefix, '')) {
681
                $key = $prefix . $joiner . $key;
682
            }
683
            $return = array_merge($return, data_flatten($value, $key, $joiner));
684
        } else {
685
            if (strcmp($prefix, '')) {
686
                $key = $prefix . $joiner . $key;
687
            }
688
            $return[$key] = $value;
689
        }
690
    }
691
692
    return $return;
693
}
694