1<?php
2/*
3 * LibreNMS - Common Functions
4 *
5 * Original Observium version by: Adam Armstrong, Tom Laermans
6 * Copyright (c) 2009-2012 Adam Armstrong.
7 *
8 * Additions for LibreNMS by: Neil Lathwood, Paul Gear, Tim DuFrane
9 * Copyright (c) 2014-2015 Neil Lathwood <http://www.lathwood.co.uk>
10 * Copyright (c) 2014-2015 Gear Consulting Pty Ltd <http://libertysys.com.au/>
11 *
12 * This program is free software: you can redistribute it and/or modify it
13 * under the terms of the GNU General Public License as published by the
14 * Free Software Foundation, either version 3 of the License, or (at your
15 * option) any later version.  Please see LICENSE.txt at the top level of
16 * the source code distribution for details.
17 */
18
19use LibreNMS\Config;
20use LibreNMS\Enum\Alert;
21use LibreNMS\Exceptions\InvalidIpException;
22use LibreNMS\Util\Debug;
23use LibreNMS\Util\Git;
24use LibreNMS\Util\IP;
25use LibreNMS\Util\Laravel;
26use Symfony\Component\Process\Process;
27
28function generate_priority_status($priority)
29{
30    $map = [
31        'emerg'     => 2,
32        'alert'     => 2,
33        'crit'      => 2,
34        'err'       => 2,
35        'warning'   => 1,
36        'notice'    => 0,
37        'info'      => 0,
38        'debug'     => 3,
39        ''          => 0,
40    ];
41
42    return isset($map[$priority]) ? $map[$priority] : 0;
43}
44
45function graylog_severity_label($severity)
46{
47    $map = [
48        '0' => 'label-danger',
49        '1' => 'label-danger',
50        '2' => 'label-danger',
51        '3' => 'label-danger',
52        '4' => 'label-warning',
53        '5' => 'label-info',
54        '6' => 'label-info',
55        '7' => 'label-default',
56        ''  => 'label-info',
57    ];
58    $barColor = isset($map[$severity]) ? $map[$severity] : 'label-info';
59
60    return '<span class="alert-status ' . $barColor . '" style="margin-right:8px;float:left;"></span>';
61}
62
63/**
64 * Execute and snmp command, filter debug output unless -v is specified
65 *
66 * @param array $command
67 * @return null|string
68 */
69function external_exec($command)
70{
71    $device = DeviceCache::getPrimary();
72
73    $proc = new Process($command);
74    $proc->setTimeout(Config::get('snmp.exec_timeout', 1200));
75
76    if (Debug::isEnabled() && ! Debug::isVerbose()) {
77        $patterns = [
78            '/-c\' \'[\S]+\'/',
79            '/-u\' \'[\S]+\'/',
80            '/-U\' \'[\S]+\'/',
81            '/-A\' \'[\S]+\'/',
82            '/-X\' \'[\S]+\'/',
83            '/-P\' \'[\S]+\'/',
84            '/-H\' \'[\S]+\'/',
85            '/(udp|udp6|tcp|tcp6):([^:]+):([\d]+)/',
86        ];
87        $replacements = [
88            '-c\' \'COMMUNITY\'',
89            '-u\' \'USER\'',
90            '-U\' \'USER\'',
91            '-A\' \'PASSWORD\'',
92            '-X\' \'PASSWORD\'',
93            '-P\' \'PASSWORD\'',
94            '-H\' \'HOSTNAME\'',
95            '\1:HOSTNAME:\3',
96        ];
97
98        $debug_command = preg_replace($patterns, $replacements, $proc->getCommandLine());
99        c_echo('SNMP[%c' . $debug_command . "%n]\n");
100    } elseif (Debug::isVerbose()) {
101        c_echo('SNMP[%c' . $proc->getCommandLine() . "%n]\n");
102    }
103
104    $proc->run();
105    $output = $proc->getOutput();
106
107    if ($proc->getExitCode()) {
108        if (Str::startsWith($proc->getErrorOutput(), 'Invalid authentication protocol specified')) {
109            Log::event('Unsupported SNMP authentication algorithm - ' . $proc->getExitCode(), optional($device)->device_id, 'poller', Alert::ERROR);
110        } elseif (Str::startsWith($proc->getErrorOutput(), 'Invalid privacy protocol specified')) {
111            Log::event('Unsupported SNMP privacy algorithm - ' . $proc->getExitCode(), optional($device)->device_id, 'poller', Alert::ERROR);
112        }
113        d_echo('Exitcode: ' . $proc->getExitCode());
114        d_echo($proc->getErrorOutput());
115    }
116
117    if (Debug::isEnabled() && ! Debug::isVerbose()) {
118        $ip_regex = '/(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/';
119        $debug_output = preg_replace($ip_regex, '*', $output);
120        d_echo($debug_output . PHP_EOL);
121    } elseif (Debug::isVerbose()) {
122        d_echo($output . PHP_EOL);
123    }
124    d_echo($proc->getErrorOutput());
125
126    return $output;
127}
128
129function shorthost($hostname, $len = 12)
130{
131    // IP addresses should not be shortened
132    if (filter_var($hostname, FILTER_VALIDATE_IP)) {
133        return $hostname;
134    }
135    $len = Config::get('shorthost_target_length', $len);
136
137    $parts = explode('.', $hostname);
138    $shorthost = $parts[0];
139    $i = 1;
140    while ($i < count($parts) && strlen($shorthost . '.' . $parts[$i]) < $len) {
141        $shorthost = $shorthost . '.' . $parts[$i];
142        $i++;
143    }
144
145    return $shorthost;
146}
147
148function print_error($text)
149{
150    if (Laravel::isCli()) {
151        c_echo('%r' . $text . "%n\n");
152    } else {
153        echo '<div class="alert alert-danger"><i class="fa fa-fw fa-exclamation-circle" aria-hidden="true"></i> ' . $text . '</div>';
154    }
155}
156
157function print_message($text)
158{
159    if (Laravel::isCli()) {
160        c_echo('%g' . $text . "%n\n");
161    } else {
162        echo '<div class="alert alert-success"><i class="fa fa-fw fa-check-circle" aria-hidden="true"></i> ' . $text . '</div>';
163    }
164}
165
166function get_sensor_rrd($device, $sensor)
167{
168    return Rrd::name($device['hostname'], get_sensor_rrd_name($device, $sensor));
169}
170
171function get_sensor_rrd_name($device, $sensor)
172{
173    // For IPMI, sensors tend to change order, and there is no index, so we prefer to use the description as key here.
174    if (Config::getOsSetting($device['os'], 'sensor_descr') || $sensor['poller_type'] == 'ipmi') {
175        return ['sensor', $sensor['sensor_class'], $sensor['sensor_type'], $sensor['sensor_descr']];
176    } else {
177        return ['sensor', $sensor['sensor_class'], $sensor['sensor_type'], $sensor['sensor_index']];
178    }
179}
180
181function get_port_rrdfile_path($hostname, $port_id, $suffix = '')
182{
183    return Rrd::name($hostname, Rrd::portName($port_id, $suffix));
184}
185
186function get_port_by_index_cache($device_id, $ifIndex)
187{
188    global $port_index_cache;
189
190    if (isset($port_index_cache[$device_id][$ifIndex]) && is_array($port_index_cache[$device_id][$ifIndex])) {
191        $port = $port_index_cache[$device_id][$ifIndex];
192    } else {
193        $port = get_port_by_ifIndex($device_id, $ifIndex);
194        $port_index_cache[$device_id][$ifIndex] = $port;
195    }
196
197    return $port;
198}
199
200function get_port_by_ifIndex($device_id, $ifIndex)
201{
202    return dbFetchRow('SELECT * FROM `ports` WHERE `device_id` = ? AND `ifIndex` = ?', [$device_id, $ifIndex]);
203}
204
205function table_from_entity_type($type)
206{
207    // Fuck you, english pluralisation.
208    if ($type == 'storage') {
209        return $type;
210    } else {
211        return $type . 's';
212    }
213}
214
215function get_entity_by_id_cache($type, $id)
216{
217    global $entity_cache;
218
219    $table = table_from_entity_type($type);
220
221    if (is_array($entity_cache[$type][$id])) {
222        $entity = $entity_cache[$type][$id];
223    } else {
224        $entity = dbFetchRow('SELECT * FROM `' . $table . '` WHERE `' . $type . '_id` = ?', [$id]);
225        $entity_cache[$type][$id] = $entity;
226    }
227
228    return $entity;
229}
230
231function get_port_by_id($port_id)
232{
233    if (is_numeric($port_id)) {
234        $port = dbFetchRow('SELECT * FROM `ports` WHERE `port_id` = ?', [$port_id]);
235        if (is_array($port)) {
236            return $port;
237        } else {
238            return false;
239        }
240    }
241}
242
243function get_application_by_id($application_id)
244{
245    if (is_numeric($application_id)) {
246        $application = dbFetchRow('SELECT * FROM `applications` WHERE `app_id` = ?', [$application_id]);
247        if (is_array($application)) {
248            return $application;
249        } else {
250            return false;
251        }
252    }
253}
254
255function get_sensor_by_id($sensor_id)
256{
257    if (is_numeric($sensor_id)) {
258        $sensor = dbFetchRow('SELECT * FROM `sensors` WHERE `sensor_id` = ?', [$sensor_id]);
259        if (is_array($sensor)) {
260            return $sensor;
261        } else {
262            return false;
263        }
264    }
265}
266
267function get_device_id_by_port_id($port_id)
268{
269    if (is_numeric($port_id)) {
270        $device_id = dbFetchCell('SELECT `device_id` FROM `ports` WHERE `port_id` = ?', [$port_id]);
271        if (is_numeric($device_id)) {
272            return $device_id;
273        } else {
274            return false;
275        }
276    }
277}
278
279function get_device_id_by_app_id($app_id)
280{
281    if (is_numeric($app_id)) {
282        $device_id = dbFetchCell('SELECT `device_id` FROM `applications` WHERE `app_id` = ?', [$app_id]);
283        if (is_numeric($device_id)) {
284            return $device_id;
285        } else {
286            return false;
287        }
288    }
289}
290
291function ifclass($ifOperStatus, $ifAdminStatus)
292{
293    // fake a port model
294    return \LibreNMS\Util\Url::portLinkDisplayClass((object) ['ifOperStatus' => $ifOperStatus, 'ifAdminStatus' => $ifAdminStatus]);
295}
296
297function device_by_name($name)
298{
299    return device_by_id_cache(getidbyname($name));
300}
301
302function accesspoint_by_id($ap_id, $refresh = '0')
303{
304    $ap = dbFetchRow('SELECT * FROM `access_points` WHERE `accesspoint_id` = ?', [$ap_id]);
305
306    return $ap;
307}
308
309function device_by_id_cache($device_id, $refresh = false)
310{
311    $model = $refresh ? DeviceCache::refresh((int) $device_id) : DeviceCache::get((int) $device_id);
312
313    $device = $model->toArray();
314    $device['location'] = $model->location->location ?? null;
315    $device['lat'] = $model->location->lat ?? null;
316    $device['lng'] = $model->location->lng ?? null;
317    $device['attribs'] = $model->getAttribs();
318    $device['vrf_lite_cisco'] = $model->vrfLites->keyBy('context_name')->toArray();
319
320    return $device;
321}
322
323function truncate($substring, $max = 50, $rep = '...')
324{
325    if (strlen($substring) < 1) {
326        $string = $rep;
327    } else {
328        $string = $substring;
329    }
330    $leave = $max - strlen($rep);
331    if (strlen($string) > $max) {
332        return substr_replace($string, $rep, $leave);
333    } else {
334        return $string;
335    }
336}
337
338function gethostbyid($device_id)
339{
340    return DeviceCache::get((int) $device_id)->hostname;
341}
342
343function strgen($length = 16)
344{
345    $entropy = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'e',
346        'E', 'f', 'F', 'g', 'G', 'h', 'H', 'i', 'I', 'j', 'J', 'k', 'K', 'l', 'L', 'm', 'M', 'n',
347        'N', 'o', 'O', 'p', 'P', 'q', 'Q', 'r', 'R', 's', 'S', 't', 'T', 'u', 'U', 'v', 'V', 'w',
348        'W', 'x', 'X', 'y', 'Y', 'z', 'Z', ];
349    $string = '';
350
351    for ($i = 0; $i < $length; $i++) {
352        $key = mt_rand(0, 61);
353        $string .= $entropy[$key];
354    }
355
356    return $string;
357}
358
359function getpeerhost($id)
360{
361    return dbFetchCell('SELECT `device_id` from `bgpPeers` WHERE `bgpPeer_id` = ?', [$id]);
362}
363
364function getifindexbyid($id)
365{
366    return dbFetchCell('SELECT `ifIndex` FROM `ports` WHERE `port_id` = ?', [$id]);
367}
368
369function getifbyid($id)
370{
371    return dbFetchRow('SELECT * FROM `ports` WHERE `port_id` = ?', [$id]);
372}
373
374function getifdescrbyid($id)
375{
376    return dbFetchCell('SELECT `ifDescr` FROM `ports` WHERE `port_id` = ?', [$id]);
377}
378
379function getidbyname($hostname)
380{
381    return DeviceCache::getByHostname($hostname)->device_id;
382}
383
384function zeropad($num, $length = 2)
385{
386    return str_pad($num, $length, '0', STR_PAD_LEFT);
387}
388
389function set_dev_attrib($device, $attrib_type, $attrib_value)
390{
391    return DeviceCache::get((int) $device['device_id'])->setAttrib($attrib_type, $attrib_value);
392}
393
394function get_dev_attribs($device_id)
395{
396    return DeviceCache::get((int) $device_id)->getAttribs();
397}
398
399function get_dev_entity_state($device)
400{
401    $state = [];
402    foreach (dbFetchRows('SELECT * FROM entPhysical_state WHERE `device_id` = ?', [$device]) as $entity) {
403        $state['group'][$entity['group']][$entity['entPhysicalIndex']][$entity['subindex']][$entity['key']] = $entity['value'];
404        $state['index'][$entity['entPhysicalIndex']][$entity['subindex']][$entity['group']][$entity['key']] = $entity['value'];
405    }
406
407    return $state;
408}
409
410function get_dev_attrib($device, $attrib_type)
411{
412    return DeviceCache::get((int) $device['device_id'])->getAttrib($attrib_type);
413}
414
415function del_dev_attrib($device, $attrib_type)
416{
417    return DeviceCache::get((int) $device['device_id'])->forgetAttrib($attrib_type);
418}
419
420/**
421 * Output using console color if possible
422 * https://github.com/pear/Console_Color2/blob/master/examples/documentation
423 *
424 * @param string $string the string to print with console color
425 * @param bool $enabled if set to false, this function does nothing
426 */
427function c_echo($string, $enabled = true)
428{
429    if (! $enabled) {
430        return;
431    }
432
433    if (Laravel::isCli()) {
434        global $console_color;
435        if ($console_color) {
436            echo $console_color->convert($string);
437        } else {
438            // limited functionality for validate.php
439            $search = [
440                '/%n/',
441                '/%g/',
442                '/%R/',
443                '/%Y/',
444                '/%B/',
445                '/%((%)|.)/', // anything left over replace with empty string
446            ];
447            $replace = [
448                "\e[0m",
449                "\e[32m",
450                "\e[1;31m",
451                "\e[1;33m",
452                "\e[1;34m",
453                '',
454            ];
455            echo preg_replace($search, $replace, $string);
456        }
457    } else {
458        echo preg_replace('/%((%)|.)/', '', $string);
459    }
460}
461
462/*
463 * @return true if client IP address is authorized to access graphs
464 */
465function is_client_authorized($clientip)
466{
467    if (Config::get('allow_unauth_graphs', false)) {
468        d_echo("Unauthorized graphs allowed\n");
469
470        return true;
471    }
472
473    foreach (Config::get('allow_unauth_graphs_cidr', []) as $range) {
474        try {
475            if (IP::parse($clientip)->inNetwork($range)) {
476                d_echo("Unauthorized graphs allowed from $range\n");
477
478                return true;
479            }
480        } catch (InvalidIpException $e) {
481            d_echo("Client IP ($clientip) is invalid.\n");
482        }
483    }
484
485    return false;
486} // is_client_authorized
487
488/*
489 * @return an array of all graph subtypes for the given type
490 */
491function get_graph_subtypes($type, $device = null)
492{
493    $type = basename($type);
494    $types = [];
495
496    // find the subtypes defined in files
497    if ($handle = opendir(Config::get('install_dir') . "/includes/html/graphs/$type/")) {
498        while (false !== ($file = readdir($handle))) {
499            if ($file != '.' && $file != '..' && $file != 'auth.inc.php' && strstr($file, '.inc.php')) {
500                $types[] = str_replace('.inc.php', '', $file);
501            }
502        }
503        closedir($handle);
504    }
505
506    sort($types);
507
508    return $types;
509} // get_graph_subtypes
510
511function get_smokeping_files($device)
512{
513    $smokeping = new \LibreNMS\Util\Smokeping(DeviceCache::get((int) $device['device_id']));
514
515    return $smokeping->findFiles();
516}
517
518function generate_smokeping_file($device, $file = '')
519{
520    $smokeping = new \LibreNMS\Util\Smokeping(DeviceCache::get((int) $device['device_id']));
521
522    return $smokeping->generateFileName($file);
523}
524
525/*
526 * @return rounded value to 10th/100th/1000th depending on input (valid: 10, 100, 1000)
527 */
528function round_Nth($val, $round_to)
529{
530    if (($round_to == '10') || ($round_to == '100') || ($round_to == '1000')) {
531        $diff = $val % $round_to;
532        if ($diff >= ($round_to / 2)) {
533            $ret = $val + ($round_to - $diff);
534        } else {
535            $ret = $val - $diff;
536        }
537
538        return $ret;
539    }
540} // end round_Nth
541
542function is_customoid_graph($type, $subtype)
543{
544    if (! empty($subtype) && $type == 'customoid') {
545        return true;
546    }
547
548    return false;
549} // is_customoid_graph
550
551//
552// maintain a simple cache of objects
553//
554
555function object_add_cache($section, $obj)
556{
557    global $object_cache;
558    $object_cache[$section][$obj] = true;
559} // object_add_cache
560
561function object_is_cached($section, $obj)
562{
563    global $object_cache;
564    if (array_key_exists($obj, $object_cache)) {
565        return $object_cache[$section][$obj];
566    } else {
567        return false;
568    }
569} // object_is_cached
570
571/**
572 * Checks if config allows us to ping this device
573 * $attribs contains an array of all of this devices
574 * attributes
575 * @param array $attribs Device attributes
576 * @return bool
577 **/
578function can_ping_device($attribs)
579{
580    if (Config::get('icmp_check') && ! (isset($attribs['override_icmp_disable']) && $attribs['override_icmp_disable'] == 'true')) {
581        return true;
582    } else {
583        return false;
584    }
585} // end can_ping_device
586
587function search_phrase_column($c)
588{
589    global $searchPhrase;
590
591    return "$c LIKE '%$searchPhrase%'";
592} // search_phrase_column
593
594/**
595 * Constructs the path to an RRD for the Ceph application
596 * @param string $gtype The type of rrd we're looking for
597 * @return string
598 **/
599function ceph_rrd($gtype)
600{
601    global $device;
602    global $vars;
603
604    if ($gtype == 'osd') {
605        $var = $vars['osd'];
606    } else {
607        $var = $vars['pool'];
608    }
609
610    return Rrd::name($device['hostname'], ['app', 'ceph', $vars['id'], $gtype, $var]);
611} // ceph_rrd
612
613/**
614 * Parse location field for coordinates
615 * @param string location The location field to look for coords in.
616 * @return array|bool Containing the lat and lng coords
617 **/
618function parse_location($location)
619{
620    preg_match('/\[(-?[0-9. ]+), *(-?[0-9. ]+)\]/', $location, $tmp_loc);
621    if (is_numeric($tmp_loc[1]) && is_numeric($tmp_loc[2])) {
622        return ['lat' => $tmp_loc[1], 'lng' => $tmp_loc[2]];
623    }
624
625    return false;
626}//end parse_location()
627
628/**
629 * Returns version info
630 * @param bool $remote fetch remote version info from github
631 * @return array
632 */
633function version_info($remote = false)
634{
635    $version = \LibreNMS\Util\Version::get();
636    $output = [
637        'local_ver' => $version->local(),
638    ];
639    if (Git::repoPresent() && Git::binaryExists()) {
640        if ($remote === true && Config::get('update_channel') == 'master') {
641            $api = curl_init();
642            set_curl_proxy($api);
643            curl_setopt($api, CURLOPT_USERAGENT, 'LibreNMS');
644            curl_setopt($api, CURLOPT_URL, Config::get('github_api') . 'commits/master');
645            curl_setopt($api, CURLOPT_RETURNTRANSFER, 1);
646            curl_setopt($api, CURLOPT_TIMEOUT, 5);
647            curl_setopt($api, CURLOPT_TIMEOUT_MS, 5000);
648            curl_setopt($api, CURLOPT_CONNECTTIMEOUT, 5);
649            $output['github'] = json_decode(curl_exec($api), true);
650        }
651        [$local_sha, $local_date] = explode('|', rtrim(`git show --pretty='%H|%ct' -s HEAD`));
652        $output['local_sha'] = $local_sha;
653        $output['local_date'] = $local_date;
654        $output['local_branch'] = rtrim(`git rev-parse --abbrev-ref HEAD`);
655    } else {
656        $output['local_ver']  = '21.5.1';
657        # obtained via git show --pretty='%H|%ct' -s 21.5.1
658        $output['local_date'] = 1621427246;
659    }
660    $output['db_schema'] = vsprintf('%s (%s)', $version->database());
661    $output['php_ver'] = phpversion();
662    $output['python_ver'] = \LibreNMS\Util\Version::python();
663    $output['mysql_ver'] = dbIsConnected() ? dbFetchCell('SELECT version()') : '?';
664    $output['rrdtool_ver'] = str_replace('1.7.01.7.0', '1.7.0', implode(' ', array_slice(explode(' ', shell_exec(
665        Config::get('rrdtool', 'rrdtool') . ' --version |head -n1'
666    )), 1, 1)));
667    $output['netsnmp_ver'] = str_replace('version: ', '', rtrim(shell_exec(
668        Config::get('snmpget', 'snmpget') . ' -V 2>&1'
669    )));
670
671    return $output;
672}//end version_info()
673
674/**
675 * Checks SNMPv3 capabilities
676 *
677 * SHA2 for Auth Algorithms (SHA-224,SHA-256,SHA-384,SHA-512)
678 * AES-192, AES-256 for Privacy Algorithms
679 */
680function snmpv3_capabilities(): array
681{
682    $process = new Process([Config::get('snmpget', 'snmpget'), '--help']);
683    $process->run();
684
685    $ret['sha2'] = Str::contains($process->getErrorOutput(), 'SHA-512');
686    $ret['aes256'] = Str::contains($process->getErrorOutput(), 'AES-256');
687
688    return $ret;
689}
690
691/**
692 * Convert a MySQL binary v4 (4-byte) or v6 (16-byte) IP address to a printable string.
693 * @param string $ip A binary string containing an IP address, as returned from MySQL's INET6_ATON function
694 * @return string Empty if not valid.
695 */
696// Fuction is from https://php.net/manual/en/function.inet-ntop.php
697function inet6_ntop($ip)
698{
699    $l = strlen($ip);
700    if ($l == 4 or $l == 16) {
701        return inet_ntop(pack('A' . $l, $ip));
702    }
703
704    return '';
705}
706
707/**
708 * If hostname is an ip, use return sysName
709 * @param array $device (uses hostname and sysName fields)
710 * @param string $hostname
711 * @return string
712 */
713function format_hostname($device, $hostname = null)
714{
715    if (empty($hostname)) {
716        $hostname = $device['hostname'];
717    }
718
719    if (Config::get('force_hostname_to_sysname') && ! empty($device['sysName'])) {
720        if (\LibreNMS\Util\Validate::hostname($hostname) && ! IP::isValid($hostname)) {
721            return $device['sysName'];
722        }
723    }
724
725    if (Config::get('force_ip_to_sysname') && ! empty($device['sysName'])) {
726        if (IP::isValid($hostname)) {
727            return $device['sysName'];
728        }
729    }
730
731    return $hostname;
732}
733
734/**
735 * Return valid port association modes
736 * @return array
737 */
738function get_port_assoc_modes()
739{
740    return [
741        1 => 'ifIndex',
742        2 => 'ifName',
743        3 => 'ifDescr',
744        4 => 'ifAlias',
745    ];
746}
747
748/**
749 * Get DB id of given port association mode name
750 * @param string $port_assoc_mode
751 * @return int
752 */
753function get_port_assoc_mode_id($port_assoc_mode)
754{
755    $modes = array_flip(get_port_assoc_modes());
756
757    return isset($modes[$port_assoc_mode]) ? $modes[$port_assoc_mode] : false;
758}
759
760/**
761 * Get name of given port association_mode ID
762 * @param int $port_assoc_mode_id Port association mode ID
763 * @return bool
764 */
765function get_port_assoc_mode_name($port_assoc_mode_id)
766{
767    $modes = get_port_assoc_modes();
768
769    return isset($modes[$port_assoc_mode_id]) ? $modes[$port_assoc_mode_id] : false;
770}
771
772/**
773 * Query all ports of the given device (by ID) and build port array and
774 * port association maps for ifIndex, ifName, ifDescr. Query port stats
775 * if told to do so, too.
776 * @param int $device_id ID of device to query ports for
777 * @param bool $with_statistics Query port statistics, too. (optional, default false)
778 * @return array
779 */
780function get_ports_mapped($device_id, $with_statistics = false)
781{
782    $ports = [];
783    $maps = [
784        'ifIndex' => [],
785        'ifName'  => [],
786        'ifDescr' => [],
787    ];
788
789    if ($with_statistics) {
790        /* ... including any related ports_statistics if requested */
791        $query = 'SELECT *, `ports_statistics`.`port_id` AS `ports_statistics_port_id`, `ports`.`port_id` AS `port_id` FROM `ports` LEFT OUTER JOIN `ports_statistics` ON `ports`.`port_id` = `ports_statistics`.`port_id` WHERE `ports`.`device_id` = ? ORDER BY ports.port_id';
792    } else {
793        /* Query all information available for ports for this device ... */
794        $query = 'SELECT * FROM `ports` WHERE `device_id` = ? ORDER BY port_id';
795    }
796
797    // Query known ports in order of discovery to make sure the latest
798    // discoverd/polled port is in the mapping tables.
799    foreach (dbFetchRows($query, [$device_id]) as $port) {
800        // Store port information by ports port_id from DB
801        $ports[$port['port_id']] = $port;
802
803        // Build maps from ifIndex, ifName, ifDescr to port_id
804        $maps['ifIndex'][$port['ifIndex']] = $port['port_id'];
805        $maps['ifName'][$port['ifName']] = $port['port_id'];
806        $maps['ifDescr'][$port['ifDescr']] = $port['port_id'];
807    }
808
809    return [
810        'ports' => $ports,
811        'maps'  => $maps,
812    ];
813}
814
815/**
816 * Calculate port_id of given port using given devices port information and port association mode
817 * @param array $ports_mapped Port information of device queried by get_ports_mapped()
818 * @param array $port Port information as fetched from DB
819 * @param string $port_association_mode Port association mode to use for mapping
820 * @return int port_id (or Null)
821 */
822function get_port_id($ports_mapped, $port, $port_association_mode)
823{
824    // Get port_id according to port_association_mode used for this device
825    $port_id = null;
826
827    /*
828     * Information an all ports is available through $ports_mapped['ports']
829     * This might come in handy sometime in the future to add you nifty new
830     * port mapping schema:
831     *
832     * $ports = $ports_mapped['ports'];
833    */
834    $maps = $ports_mapped['maps'];
835
836    if (in_array($port_association_mode, ['ifIndex', 'ifName', 'ifDescr', 'ifAlias'])) {
837        $port_id = $maps[$port_association_mode][$port[$port_association_mode]];
838    }
839
840    return $port_id;
841}
842
843/**
844 * Create a glue-chain
845 * @param array $tables Initial Tables to construct glue-chain
846 * @param string $target Glue to find (usual device_id)
847 * @param int $x Recursion Anchor
848 * @param array $hist History of processed tables
849 * @param array $last Glues on the fringe
850 * @return array|false
851 */
852function ResolveGlues($tables, $target, $x = 0, $hist = [], $last = [])
853{
854    if (sizeof($tables) == 1 && $x != 0) {
855        if (dbFetchCell('SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_NAME = ? && COLUMN_NAME = ?', [$tables[0], $target]) == 1) {
856            return array_merge($last, [$tables[0] . '.' . $target]);
857        } else {
858            return false;
859        }
860    } else {
861        $x++;
862        if ($x > 30) {
863            //Too much recursion. Abort.
864            return false;
865        }
866        foreach ($tables as $table) {
867            if ($table == 'state_translations' && ($target == 'device_id' || $target == 'sensor_id')) {
868                // workaround for state_translations
869                return array_merge($last, [
870                    'state_translations.state_index_id',
871                    'sensors_to_state_indexes.sensor_id',
872                    "sensors.$target",
873                ]);
874            } elseif ($table == 'application_metrics' && $target == 'device_id') {
875                return array_merge($last, [
876                    'application_metrics.app_id',
877                    "applications.$target",
878                ]);
879            } elseif ($table == 'locations' && $target == 'device_id') {
880                return array_merge($last, [
881                    'locations.id',
882                    'devices.device_id.location_id',
883                ]);
884            }
885
886            $glues = dbFetchRows('SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = ? && COLUMN_NAME LIKE "%\_id"', [$table]);
887            if (sizeof($glues) == 1 && $glues[0]['COLUMN_NAME'] != $target) {
888                //Search for new candidates to expand
889                $ntables = [];
890                [$tmp] = explode('_', $glues[0]['COLUMN_NAME'], 2);
891                $ntables[] = $tmp;
892                $ntables[] = $tmp . 's';
893                $tmp = dbFetchRows('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_NAME LIKE "' . substr($table, 0, -1) . '_%" && TABLE_NAME != "' . $table . '"');
894                foreach ($tmp as $expand) {
895                    $ntables[] = $expand['TABLE_NAME'];
896                }
897                $tmp = ResolveGlues($ntables, $target, $x++, array_merge($tables, $ntables), array_merge($last, [$table . '.' . $glues[0]['COLUMN_NAME']]));
898                if (is_array($tmp)) {
899                    return $tmp;
900                }
901            } else {
902                foreach ($glues as $glue) {
903                    if ($glue['COLUMN_NAME'] == $target) {
904                        return array_merge($last, [$table . '.' . $target]);
905                    } else {
906                        [$tmp] = explode('_', $glue['COLUMN_NAME']);
907                        $tmp .= 's';
908                        if (! in_array($tmp, $tables) && ! in_array($tmp, $hist)) {
909                            //Expand table
910                            $tmp = ResolveGlues([$tmp], $target, $x++, array_merge($tables, [$tmp]), array_merge($last, [$table . '.' . $glue['COLUMN_NAME']]));
911                            if (is_array($tmp)) {
912                                return $tmp;
913                            }
914                        }
915                    }
916                }
917            }
918        }
919    }
920    //You should never get here.
921    return false;
922}
923
924/**
925 * Determine if a given string contains a given substring.
926 *
927 * @param  string $haystack
928 * @param  string|array $needles
929 * @return bool
930 */
931function str_i_contains($haystack, $needles)
932{
933    foreach ((array) $needles as $needle) {
934        if ($needle != '' && stripos($haystack, $needle) !== false) {
935            return true;
936        }
937    }
938
939    return false;
940}
941
942/**
943 * Get alert_rules sql filter by minimal severity
944 *
945 * @param  string|int $min_severity
946 * @param  string $alert_rules_name
947 * @return string
948 */
949function get_sql_filter_min_severity($min_severity, $alert_rules_name)
950{
951    $alert_severities = [
952        // alert_rules.status is enum('ok','warning','critical')
953        'ok' => 1,
954        'warning' => 2,
955        'critical' => 3,
956        'ok only' => 4,
957        'warning only' => 5,
958        'critical only' => 6,
959    ];
960    if (is_numeric($min_severity)) {
961        $min_severity_id = $min_severity;
962    } elseif (! empty($min_severity)) {
963        $min_severity_id = $alert_severities[$min_severity];
964    }
965    if (isset($min_severity_id)) {
966        return " AND `$alert_rules_name`.`severity` " . ($min_severity_id > 3 ? '' : '>') . '= ' . ($min_severity_id > 3 ? $min_severity_id - 3 : $min_severity_id);
967    }
968
969    return '';
970}
971
972/**
973 * Load the os definition for the device and set type and os_group
974 * $device['os'] must be set
975 *
976 * @param array $device
977 */
978function load_os(&$device)
979{
980    if (! isset($device['os'])) {
981        d_echo("No OS to load\n");
982
983        return;
984    }
985
986    \LibreNMS\Util\OS::loadDefinition($device['os']);
987
988    // Set type to a predefined type for the OS if it's not already set
989    $loaded_os_type = Config::get("os.{$device['os']}.type");
990    if ((! isset($device['attribs']['override_device_type']) && $device['attribs']['override_device_type'] != 1) && array_key_exists('type', $device) && $loaded_os_type != $device['type']) {
991        log_event('Device type changed ' . $device['type'] . ' => ' . $loaded_os_type, $device, 'system', 3);
992        $device['type'] = $loaded_os_type;
993        dbUpdate(['type' => $loaded_os_type], 'devices', 'device_id=?', [$device['device_id']]);
994        d_echo("Device type changed to $loaded_os_type!\n");
995    }
996
997    if ($os_group = Config::get("os.{$device['os']}.group")) {
998        $device['os_group'] = $os_group;
999    } else {
1000        unset($device['os_group']);
1001    }
1002}
1003
1004/**
1005 * Converts fahrenheit to celsius (with 2 decimal places)
1006 * if $scale is not fahrenheit, it assumes celsius and  returns the value
1007 *
1008 * @param float $value
1009 * @param string $scale fahrenheit or celsius
1010 * @return string (containing a float)
1011 */
1012function fahrenheit_to_celsius($value, $scale = 'fahrenheit')
1013{
1014    if ($scale === 'fahrenheit') {
1015        $value = ($value - 32) / 1.8;
1016    }
1017
1018    return sprintf('%.02f', $value);
1019}
1020
1021/**
1022 * Converts celsius to fahrenheit (with 2 decimal places)
1023 * if $scale is not celsius, it assumes celsius and  returns the value
1024 *
1025 * @param float $value
1026 * @param string $scale fahrenheit or celsius
1027 * @return string (containing a float)
1028 */
1029function celsius_to_fahrenheit($value, $scale = 'celsius')
1030{
1031    if ($scale === 'celsius') {
1032        $value = ($value * 1.8) + 32;
1033    }
1034
1035    return sprintf('%.02f', $value);
1036}
1037
1038/**
1039 * Converts string to float
1040 */
1041function string_to_float($value)
1042{
1043    return sprintf('%.02f', $value);
1044}
1045
1046/**
1047 * Converts uW to dBm
1048 * $value must be positive
1049 */
1050function uw_to_dbm($value)
1051{
1052    return 10 * log10($value / 1000);
1053}
1054
1055/**
1056 * Converts mW to dBm
1057 * $value must be positive
1058 */
1059function mw_to_dbm($value)
1060{
1061    return 10 * log10($value);
1062}
1063
1064/**
1065 * @param $value
1066 * @param null $default
1067 * @param int $min
1068 * @return null
1069 */
1070function set_null($value, $default = null, $min = null)
1071{
1072    if (! is_numeric($value)) {
1073        return $default;
1074    } elseif (is_nan($value)) {
1075        return $default;
1076    } elseif (is_infinite($value)) {
1077        return $default;
1078    } elseif (isset($min) && $value < $min) {
1079        return $default;
1080    }
1081
1082    return $value;
1083}
1084/*
1085 * @param $value
1086 * @param int $default
1087 * @return int
1088 */
1089function set_numeric($value, $default = 0)
1090{
1091    if (! is_numeric($value) ||
1092        is_nan($value) ||
1093        is_infinite($value)
1094    ) {
1095        $value = $default;
1096    }
1097
1098    return $value;
1099}
1100
1101function get_vm_parent_id($device)
1102{
1103    if (empty($device['hostname'])) {
1104        return false;
1105    }
1106
1107    return dbFetchCell('SELECT `device_id` FROM `vminfo` WHERE `vmwVmDisplayName` = ? OR `vmwVmDisplayName` = ?', [$device['hostname'], $device['hostname'] . '.' . Config::get('mydomain')]);
1108}
1109
1110/**
1111 * Generate a class name from a lowercase string containing - or _
1112 * Remove - and _ and camel case words
1113 *
1114 * @param string $name The string to convert to a class name
1115 * @param string $namespace namespace to prepend to the name for example: LibreNMS\
1116 * @return string  Class name
1117 */
1118function str_to_class($name, $namespace = null)
1119{
1120    $pre_format = str_replace(['-', '_'], ' ', $name);
1121    $class = str_replace(' ', '', ucwords(strtolower($pre_format)));
1122    $class = preg_replace_callback('/^(\d)(.)/', function ($matches) {
1123        $numbers = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine'];
1124
1125        return $numbers[$matches[1]] . strtoupper($matches[2]);
1126    }, $class);
1127
1128    return $namespace . $class;
1129}
1130
1131/**
1132 * Index an array by a column
1133 *
1134 * @param array $array
1135 * @param string|int $column
1136 * @return array
1137 */
1138function array_by_column($array, $column)
1139{
1140    return array_combine(array_column($array, $column), $array);
1141}
1142