diff --git a/LibreNMS/Alert/AlertDB.php b/LibreNMS/Alert/AlertDB.php new file mode 100644 index 0000000000..50e9ea5264 --- /dev/null +++ b/LibreNMS/Alert/AlertDB.php @@ -0,0 +1,118 @@ +. + * + * + * Original code: + * @author Daniel Preussker + * @copyright 2014 f0o, LibreNMS + * @license GPL + * + * Modified by: + * @package LibreNMS + * @link http://librenms.org + * @copyright 2019 KanREN, Inc. + * @author Heath Barnhart + */ + +namespace LibreNMS\Alert; + +use App\Models\Device; +use LibreNMS\Alert\AlertUtil; +use LibreNMS\Alerting\QueryBuilderParser; + +class AlertDB +{ + + /** + * @param $rule + * @param $query_builder + * @return bool|string + */ + public static function genSQL($rule, $query_builder = false) + { + if ($query_builder) { + return QueryBuilderParser::fromJson($query_builder)->toSql(); + } else { + return self::genSQLOld($rule); + } + } + + /** + * Generate SQL from Rule + * @param string $rule Rule to generate SQL for + * @return string|boolean + */ + public static function genSQLOld($rule) + { + $rule = AlertUtil::runMacros($rule); + if (empty($rule)) { + //Cannot resolve Macros due to recursion. Rule is invalid. + return false; + } + //Pretty-print rule to dissect easier + $pretty = array('&&' => ' && ', '||' => ' || '); + $rule = str_replace(array_keys($pretty), $pretty, $rule); + $tmp = explode(" ", $rule); + $tables = array(); + foreach ($tmp as $opt) { + if (strstr($opt, '%') && strstr($opt, '.')) { + $tmpp = explode(".", $opt, 2); + $tmpp[0] = str_replace("%", "", $tmpp[0]); + $tables[] = mres(str_replace("(", "", $tmpp[0])); + $rule = str_replace($opt, $tmpp[0].'.'.$tmpp[1], $rule); + } + } + $tables = array_keys(array_flip($tables)); + if (dbFetchCell('SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_NAME = ? && COLUMN_NAME = ?', array($tables[0],'device_id')) != 1) { + //Our first table has no valid glue, append the 'devices' table to it! + array_unshift($tables, 'devices'); + } + $x = sizeof($tables)-1; + $i = 0; + $join = ""; + while ($i < $x) { + if (isset($tables[$i+1])) { + $gtmp = ResolveGlues(array($tables[$i+1]), 'device_id'); + if ($gtmp === false) { + //Cannot resolve glue-chain. Rule is invalid. + return false; + } + $last = ""; + $qry = ""; + foreach ($gtmp as $glue) { + if (empty($last)) { + list($tmp,$last) = explode('.', $glue); + $qry .= $glue.' = '; + } else { + list($tmp,$new) = explode('.', $glue); + $qry .= $tmp.'.'.$last.' && '.$tmp.'.'.$new.' = '; + $last = $new; + } + if (!in_array($tmp, $tables)) { + $tables[] = $tmp; + } + } + $join .= "( ".$qry.$tables[0].".device_id ) && "; + } + $i++; + } + $sql = "SELECT * FROM ".implode(",", $tables)." WHERE (".$join."".str_replace("(", "", $tables[0]).".device_id = ?) && (".str_replace(array("%","@","!~","~"), array("",".*","NOT REGEXP","REGEXP"), $rule).")"; + return $sql; + } +} diff --git a/LibreNMS/Alert/AlertRules.php b/LibreNMS/Alert/AlertRules.php new file mode 100644 index 0000000000..50f41e9521 --- /dev/null +++ b/LibreNMS/Alert/AlertRules.php @@ -0,0 +1,129 @@ +. + * + * Original Code: + * @author Daniel Preussker + * @copyright 2014 f0o, LibreNMS + * @license GPL + * @package LibreNMS + * @subpackage Alerts + * + * Modeified by: + * @package LibreNMS + * @link http://librenms.org + * @copyright 2019 KanREN, Inc. + * @author Heath Barnhart + */ + +namespace LibreNMS\Alert; + +use App\Models\Device; +use LibreNMS\Alert\AlertUtil; +use LibreNMS\Alert\AlertDB; + +class AlertRules +{ + public function runRules($device_id) + { + + //Check to see if under maintenance + if (AlertUtil::isMaintenance($device_id) > 0) { + echo "Under Maintenance, Skipping alerts.\r\n"; + return false; + } + //Checks each rule. + foreach (AlertUtil::getRules($device_id) as $rule) { + c_echo('Rule %p#'.$rule['id'].' (' . $rule['name'] . '):%n '); + $extra = json_decode($rule['extra'], true); + if (isset($extra['invert'])) { + $inv = (bool) $extra['invert']; + } else { + $inv = false; + } + d_echo(PHP_EOL); + if (empty($rule['query'])) { + $rule['query'] = AlertDB::genSQL($rule['rule'], $rule['builder']); + } + $sql = $rule['query']; + $qry = dbFetchRows($sql, array($device_id)); + $cnt = count($qry); + for ($i = 0; $i < $cnt; $i++) { + if (isset($qry[$i]['ip'])) { + $qry[$i]['ip'] = inet6_ntop($qry[$i]['ip']); + } + } + $s = sizeof($qry); + if ($s == 0 && $inv === false) { + $doalert = false; + } elseif ($s > 0 && $inv === false) { + $doalert = true; + } elseif ($s == 0 && $inv === true) { + $doalert = true; + } else { + $doalert = false; + } + + $current_state = dbFetchCell("SELECT state FROM alerts WHERE rule_id = ? AND device_id = ? ORDER BY id DESC LIMIT 1", [$rule['id'], $device_id]); + if ($doalert) { + if ($current_state == 2) { + c_echo('Status: %ySKIP'); + } elseif ($current_state >= 1) { + c_echo('Status: %bNOCHG'); + // NOCHG here doesn't mean no change full stop. It means no change to the alert state + // So we update the details column with any fresh changes to the alert output we might have. + $alert_log = dbFetchRow('SELECT alert_log.id, alert_log.details FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0 + ORDER BY alert_log.id DESC LIMIT 1', array($device_id, $rule['id'])); + $details = []; + if (!empty($alert_log['details'])) { + $details = json_decode(gzuncompress($alert_log['details']), true); + } + $details['contacts'] = AlertUtil::getContacts($qry); + $details['rule'] = $qry; + $details = gzcompress(json_encode($details), 9); + dbUpdate(array('details' => $details), 'alert_log', 'id = ?', array($alert_log['id'])); + } else { + $extra = gzcompress(json_encode(array('contacts' => AlertUtil::getContacts($qry), 'rule'=>$qry)), 9); + if (dbInsert(['state' => 1, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'details' => $extra], 'alert_log')) { + if (is_null($current_state)) { + dbInsert(array('state' => 1, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'open' => 1,'alerted' => 0), 'alerts'); + } else { + dbUpdate(['state' => 1, 'open' => 1], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]); + } + c_echo(PHP_EOL . 'Status: %rALERT'); + } + } + } else { + if (!is_null($current_state) && $current_state == 0) { + c_echo('Status: %bNOCHG'); + } else { + if (dbInsert(['state' => 0, 'device_id' => $device_id, 'rule_id' => $rule['id']], 'alert_log')) { + if (is_null($current_state)) { + dbInsert(['state' => 0, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'open' => 1, 'alerted' => 0], 'alerts'); + } else { + dbUpdate(['state' => 0, 'open' => 1, 'note' => ''], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]); + } + + c_echo(PHP_EOL . 'Status: %gOK'); + } + } + } + c_echo('%n' . PHP_EOL); + } + } +} diff --git a/LibreNMS/Alert/AlertUtil.php b/LibreNMS/Alert/AlertUtil.php index 7e5c701180..34d61924f1 100644 --- a/LibreNMS/Alert/AlertUtil.php +++ b/LibreNMS/Alert/AlertUtil.php @@ -1,6 +1,35 @@ . + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2019 KanREN, Inc. + * @author Heath Barnhart + */ + namespace LibreNMS\Alert; +use App\Models\Device; +use LibreNMS\Authentication\LegacyAuth; +use LibreNMS\Config; +use PHPMailer\PHPMailer\PHPMailer; + class AlertUtil { /** @@ -41,4 +70,156 @@ class AlertUtil $query = "SELECT transport_id, transport_type, transport_name FROM alert_transports WHERE is_default=true"; return dbFetchRows($query); } + + /** + * Find contacts for alert + * @param array $results Rule-Result + * @return array + */ + public static function getContacts($results) + { + if (empty($results)) { + return []; + } + if (Config::get('alert.default_only') === true || Config::get('alerts.email.default_only') === true) { + $email = Config::get('alert.default_mail', Config::get('alerts.email.default')); + return $email ? [$email => ''] : []; + } + $users = LegacyAuth::get()->getUserlist(); + $contacts = array(); + $uids = array(); + foreach ($results as $result) { + $tmp = null; + if (is_numeric($result["bill_id"])) { + $tmpa = dbFetchRows("SELECT user_id FROM bill_perms WHERE bill_id = ?", array($result["bill_id"])); + foreach ($tmpa as $tmp) { + $uids[$tmp['user_id']] = $tmp['user_id']; + } + } + if (is_numeric($result["port_id"])) { + $tmpa = dbFetchRows("SELECT user_id FROM ports_perms WHERE port_id = ?", array($result["port_id"])); + foreach ($tmpa as $tmp) { + $uids[$tmp['user_id']] = $tmp['user_id']; + } + } + if (is_numeric($result["device_id"])) { + if (Config::get('alert.syscontact') == true) { + if (dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_bool' AND device_id = ?", [$result["device_id"]])) { + $tmpa = dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_string' AND device_id = ?", array($result["device_id"])); + } else { + $tmpa = dbFetchCell("SELECT sysContact FROM devices WHERE device_id = ?", array($result["device_id"])); + } + if (!empty($tmpa)) { + $contacts[$tmpa] = ''; + } + } + $tmpa = dbFetchRows("SELECT user_id FROM devices_perms WHERE device_id = ?", array($result["device_id"])); + foreach ($tmpa as $tmp) { + $uids[$tmp['user_id']] = $tmp['user_id']; + } + } + } + foreach ($users as $user) { + if (empty($user['email'])) { + continue; // no email, skip this user + } + if (empty($user['realname'])) { + $user['realname'] = $user['username']; + } + if (empty($user['level'])) { + $user['level'] = LegacyAuth::get()->getUserlevel($user['username']); + } + if (Config::get('alert.globals') && ( $user['level'] >= 5 && $user['level'] < 10 )) { + $contacts[$user['email']] = $user['realname']; + } elseif (Config::get('alert.admins') && $user['level'] == 10) { + $contacts[$user['email']] = $user['realname']; + } elseif (Config::get('alert.users') == true && in_array($user['user_id'], $uids)) { + $contacts[$user['email']] = $user['realname']; + } + } + + $tmp_contacts = array(); + foreach ($contacts as $email => $name) { + if (strstr($email, ',')) { + $split_contacts = preg_split('/[,\s]+/', $email); + foreach ($split_contacts as $split_email) { + if (!empty($split_email)) { + $tmp_contacts[$split_email] = $name; + } + } + } else { + $tmp_contacts[$email] = $name; + } + } + + if (!empty($tmp_contacts)) { + // Validate contacts so we can fall back to default if configured. + $mail = new PHPMailer(); + foreach ($tmp_contacts as $tmp_email => $tmp_name) { + if ($mail->validateAddress($tmp_email) != true) { + unset($tmp_contacts[$tmp_email]); + } + } + } + + # Copy all email alerts to default contact if configured. + $default_mail = Config::get('alert.default_mail'); + if (!isset($tmp_contacts[$default_mail]) && Config::get('alert.default_copy')) { + $tmp_contacts[$default_mail] = ''; + } + # Send email to default contact if no other contact found + if (empty($tmp_contacts) && Config::get('alert.default_if_none') && $default_mail) { + $tmp_contacts[$default_mail] = ''; + } + + return $tmp_contacts; + } + + public static function getRules($device_id) + { + $query = "SELECT DISTINCT a.* FROM alert_rules a + LEFT JOIN alert_device_map d ON a.id=d.rule_id + LEFT JOIN alert_group_map g ON a.id=g.rule_id + LEFT JOIN device_group_device dg ON g.group_id=dg.device_group_id + WHERE a.disabled = 0 AND ((d.device_id IS NULL AND g.group_id IS NULL) OR d.device_id=? OR dg.device_id=?)"; + + $params = [$device_id, $device_id]; + return dbFetchRows($query, $params); + } + + /** + * Check if device is under maintenance + * @param int $device_id Device-ID + * @return bool + */ + public static function isMaintenance($device_id) + { + $device = Device::find($device_id); + return !is_null($device) && $device->isUnderMaintenance(); + } + + /** + * Process Macros + * @param string $rule Rule to process + * @param int $x Recursion-Anchor + * @return string|boolean + */ + public static function runMacros($rule, $x = 1) + { + $macros = Config::get('alert.macros.rule', []) . + krsort($macros); + foreach ($macros as $macro => $value) { + if (!strstr($macro, " ")) { + $rule = str_replace('%macros.'.$macro, '('.$value.')', $rule); + } + } + if (strstr($rule, "%macros.")) { + if (++$x < 30) { + $rule = self::runMacros($rule, $x); + } else { + return false; + } + } + return $rule; + } } diff --git a/LibreNMS/Alert/RunAlerts.php b/LibreNMS/Alert/RunAlerts.php new file mode 100644 index 0000000000..66287b0e54 --- /dev/null +++ b/LibreNMS/Alert/RunAlerts.php @@ -0,0 +1,582 @@ + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Original Code: + * @author Daniel Preussker + * @copyright 2014 f0o, LibreNMS + * @license GPL + * @package LibreNMS + * @subpackage Alerts + * + * Modified by: + * @author Heath Barnhart + * + */ + +namespace LibreNMS\Alert; + +use App\Models\DevicePerf; +use LibreNMS\Config; +use LibreNMS\Util\Time; +use Log; + +class RunAlerts +{ + + /** + * Populate variables + * @param string $txt Text with variables + * @param boolean $wrap Wrap variable for text-usage (default: true) + * @return string + */ + public function populate($txt, $wrap = true) + { + preg_match_all('/%([\w\.]+)/', $txt, $m); + foreach ($m[1] as $tmp) { + $orig = $tmp; + $rep = false; + if ($tmp == 'key' || $tmp == 'value') { + $rep = '$'.$tmp; + } else { + if (strstr($tmp, '.')) { + $tmp = explode('.', $tmp, 2); + $pre = '$'.$tmp[0]; + $tmp = $tmp[1]; + } else { + $pre = '$obj'; + } + + $rep = $pre."['".str_replace('.', "']['", $tmp)."']"; + if ($wrap) { + $rep = '{'.$rep.'}'; + } + } + + $txt = str_replace('%'.$orig, $rep, $txt); + } + return $txt; + } + + /** + * Describe Alert + * @param array $alert Alert-Result from DB + * @return array|boolean + */ + public function describeAlert($alert) + { + $obj = array(); + $i = 0; + $device = dbFetchRow('SELECT hostname, sysName, sysDescr, sysContact, os, type, ip, hardware, version, purpose, notes, uptime, status, status_reason, locations.location FROM devices LEFT JOIN locations ON locations.id = devices.location_id WHERE device_id = ?', array($alert['device_id'])); + $attribs = get_dev_attribs($alert['device_id']); + + $obj['hostname'] = $device['hostname']; + $obj['sysName'] = $device['sysName']; + $obj['sysDescr'] = $device['sysDescr']; + $obj['sysContact'] = $device['sysContact']; + $obj['os'] = $device['os']; + $obj['type'] = $device['type']; + $obj['ip'] = inet6_ntop($device['ip']); + $obj['hardware'] = $device['hardware']; + $obj['version'] = $device['version']; + $obj['location'] = $device['location']; + $obj['uptime'] = $device['uptime']; + $obj['uptime_short'] = Time::formatInterval($device['uptime'], 'short'); + $obj['uptime_long'] = Time::formatInterval($device['uptime']); + $obj['description'] = $device['purpose']; + $obj['notes'] = $device['notes']; + $obj['alert_notes'] = $alert['note']; + $obj['device_id'] = $alert['device_id']; + $obj['rule_id'] = $alert['rule_id']; + $obj['status'] = $device['status']; + $obj['status_reason'] = $device['status_reason']; + if (can_ping_device($attribs)) { + $ping_stats = DevicePerf::where('device_id', $alert['device_id'])->latest('timestamp')->first(); + $obj['ping_timestamp'] = $ping_stats->template; + $obj['ping_loss'] = $ping_stats->loss; + $obj['ping_min'] = $ping_stats->min; + $obj['ping_max'] = $ping_stats->max; + $obj['ping_avg'] = $ping_stats->avg; + $obj['debug'] = json_decode($ping_stats->debug, true); + } + $extra = $alert['details']; + + $tpl = new Template; + $template = $tpl->getTemplate($obj); + + if ($alert['state'] >= 1) { + $obj['title'] = $template->title ?: 'Alert for device '.$device['hostname'].' - '.($alert['name'] ? $alert['name'] : $alert['rule']); + if ($alert['state'] == 2) { + $obj['title'] .= ' got acknowledged'; + } elseif ($alert['state'] == 3) { + $obj['title'] .= ' got worse'; + } elseif ($alert['state'] == 4) { + $obj['title'] .= ' got better'; + } + + foreach ($extra['rule'] as $incident) { + $i++; + $obj['faults'][$i] = $incident; + $obj['faults'][$i]['string'] = null; + foreach ($incident as $k => $v) { + if (!empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) { + $obj['faults'][$i]['string'] .= $k.' = '.$v.'; '; + } + } + } + $obj['elapsed'] = $this->timeFormat(time() - strtotime($alert['time_logged'])); + if (!empty($extra['diff'])) { + $obj['diff'] = $extra['diff']; + } + } elseif ($alert['state'] == 0) { + // Alert is now cleared + $id = dbFetchRow('SELECT alert_log.id,alert_log.time_logged,alert_log.details FROM alert_log WHERE alert_log.state != 2 && alert_log.state != 0 && alert_log.rule_id = ? && alert_log.device_id = ? && alert_log.id < ? ORDER BY id DESC LIMIT 1', array($alert['rule_id'], $alert['device_id'], $alert['id'])); + if (empty($id['id'])) { + return false; + } + + $extra = []; + if (!empty($id['details'])) { + $extra = json_decode(gzuncompress($id['details']), true); + } + + // Reset count to 0 so alerts will continue + $extra['count'] = 0; + dbUpdate(array('details' => gzcompress(json_encode($id['details']), 9)), 'alert_log', 'id = ?', array($alert['id'])); + + $obj['title'] = $template->title_rec ?: 'Device '.$device['hostname'].' recovered from '.($alert['name'] ? $alert['name'] : $alert['rule']); + $obj['elapsed'] = $this->timeFormat(strtotime($alert['time_logged']) - strtotime($id['time_logged'])); + $obj['id'] = $id['id']; + foreach ($extra['rule'] as $incident) { + $i++; + $obj['faults'][$i] = $incident; + foreach ($incident as $k => $v) { + if (!empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) { + $obj['faults'][$i]['string'] .= $k.' => '.$v.'; '; + } + } + } + } else { + return 'Unknown State'; + }//end if + $obj['builder'] = $alert['builder']; + $obj['uid'] = $alert['id']; + $obj['alert_id'] = $alert['alert_id']; + $obj['severity'] = $alert['severity']; + $obj['rule'] = $alert['rule']; + $obj['name'] = $alert['name']; + $obj['timestamp'] = $alert['time_logged']; + $obj['contacts'] = $extra['contacts']; + $obj['state'] = $alert['state']; + $obj['template'] = $template; + return $obj; + } + + /** + * Format Elapsed Time + * @param integer $secs Seconds elapsed + * @return string + */ + public function timeFormat($secs) + { + $bit = array( + 'y' => $secs / 31556926 % 12, + 'w' => $secs / 604800 % 52, + 'd' => $secs / 86400 % 7, + 'h' => $secs / 3600 % 24, + 'm' => $secs / 60 % 60, + 's' => $secs % 60, + ); + $ret = array(); + foreach ($bit as $k => $v) { + if ($v > 0) { + $ret[] = $v.$k; + } + } + + if (empty($ret)) { + return 'none'; + } + + return join(' ', $ret); + } + + public function clearStaleAlerts() + { + $sql = "SELECT `alerts`.`id` AS `alert_id`, `devices`.`hostname` AS `hostname` FROM `alerts` LEFT JOIN `devices` ON `alerts`.`device_id`=`devices`.`device_id` RIGHT JOIN `alert_rules` ON `alerts`.`rule_id`=`alert_rules`.`id` WHERE `alerts`.`state`!=0 AND `devices`.`hostname` IS NULL"; + foreach (dbFetchRows($sql) as $alert) { + if (empty($alert['hostname']) && isset($alert['alert_id'])) { + dbDelete('alerts', '`id` = ?', array($alert['alert_id'])); + echo "Stale-alert: #{$alert['alert_id']}" . PHP_EOL; + } + } + } + + /** + * Re-Validate Rule-Mappings + * @param integer $device_id Device-ID + * @param integer $rule Rule-ID + * @return boolean + */ + public function isRuleValid($device_id, $rule) + { + global $rulescache; + if (empty($rulescache[$device_id]) || !isset($rulescache[$device_id])) { + foreach (AlertUtil::GetRules($device_id) as $chk) { + $rulescache[$device_id][$chk['id']] = true; + } + } + + if ($rulescache[$device_id][$rule] === true) { + return true; + } + + return false; + } + + + /** + * Issue Alert-Object + * @param array $alert + * @return boolean + */ + public function issueAlert($alert) + { + if (dbFetchCell('SELECT attrib_value FROM devices_attribs WHERE attrib_type = "disable_notify" && device_id = ?', array($alert['device_id'])) == '1') { + return true; + } + + if (Config::get('alert.fixed-contacts') == false) { + if (empty($alert['query'])) { + $alert['query'] = AlertDB::genSQL($alert['rule'], $alert['builder']); + } + $sql = $alert['query']; + $qry = dbFetchRows($sql, array($alert['device_id'])); + $alert['details']['contacts'] = AlertUtil::GetContacts($qry); + } + + $obj = $this->describeAlert($alert); + if (is_array($obj)) { + echo 'Issuing Alert-UID #'.$alert['id'].'/'.$alert['state'].':' . PHP_EOL; + $this->extTransports($obj); + + echo "\r\n"; + } + + return true; + } + + + /** + * Issue ACK notification + * @return void + */ + public function runAcks() + { + + foreach ($this->loadAlerts('alerts.state = 2 && alerts.open = 1') as $alert) { + $this->issueAlert($alert); + dbUpdate(array('open' => 0), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); + } + } + + /** + * Run Follow-Up alerts + * @return void + */ + public function runFollowUp() + { + foreach ($this->loadAlerts('alerts.state > 0 && alerts.open = 0') as $alert) { + if ($alert['state'] != 2 || ($alert['info']['until_clear'] === false)) { + $rextra = json_decode($alert['extra'], true); + if ($rextra['invert']) { + continue; + } + + if (empty($alert['query'])) { + $alert['query'] = AlertDB::genSQL($alert['rule'], $alert['builder']); + } + $chk = dbFetchRows($alert['query'], array($alert['device_id'])); + //make sure we can json_encode all the datas later + $cnt = count($chk); + for ($i = 0; $i < $cnt; $i++) { + if (isset($chk[$i]['ip'])) { + $chk[$i]['ip'] = inet6_ntop($chk[$i]['ip']); + } + } + $o = sizeof($alert['details']['rule']); + $n = sizeof($chk); + $ret = 'Alert #' . $alert['id']; + $state = 0; + if ($n > $o) { + $ret .= ' Worsens'; + $state = 3; + $alert['details']['diff'] = array_diff($chk, $alert['details']['rule']); + } elseif ($n < $o) { + $ret .= ' Betters'; + $state = 4; + $alert['details']['diff'] = array_diff($alert['details']['rule'], $chk); + } + + if ($state > 0 && $n > 0) { + $alert['details']['rule'] = $chk; + if (dbInsert(array( + 'state' => $state, + 'device_id' => $alert['device_id'], + 'rule_id' => $alert['rule_id'], + 'details' => gzcompress(json_encode($alert['details']), 9) + ), 'alert_log')) { + dbUpdate(array('state' => $state, 'open' => 1, 'alerted' => 1), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); + } + + echo $ret . ' (' . $o . '/' . $n . ")\r\n"; + } + } + } + } + + public function loadAlerts($where) + { + $alerts = []; + foreach (dbFetchRows("SELECT alerts.id, alerts.device_id, alerts.rule_id, alerts.state, alerts.note, alerts.info FROM alerts WHERE $where") as $alert_status) { + $alert = dbFetchRow( + 'SELECT alert_log.id,alert_log.rule_id,alert_log.device_id,alert_log.state,alert_log.details,alert_log.time_logged,alert_rules.rule,alert_rules.severity,alert_rules.extra,alert_rules.name,alert_rules.query,alert_rules.builder FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0 ORDER BY alert_log.id DESC LIMIT 1', + array($alert_status['device_id'], $alert_status['rule_id']) + ); + + if (empty($alert['rule_id']) || !$this->isRuleValid($alert_status['device_id'], $alert_status['rule_id'])) { + echo 'Stale-Rule: #' . $alert_status['rule_id'] . '/' . $alert_status['device_id'] . "\r\n"; + // Alert-Rule does not exist anymore, let's remove the alert-state. + dbDelete('alerts', 'rule_id = ? && device_id = ?', [$alert_status['rule_id'], $alert_status['device_id']]); + } else { + $alert['alert_id'] = $alert_status['id']; + $alert['state'] = $alert_status['state']; + $alert['note'] = $alert_status['note']; + if (!empty($alert['details'])) { + $alert['details'] = json_decode(gzuncompress($alert['details']), true); + } + $alert['info'] = json_decode($alert_status['info'], true); + $alerts[] = $alert; + } + } + + return $alerts; + } + + /** + * Run all alerts + * @return void + */ + public function runAlerts() + { + foreach ($this->loadAlerts('alerts.state != 2 && alerts.open = 1') as $alert) { + $noiss = false; + $noacc = false; + $updet = false; + $rextra = json_decode($alert['extra'], true); + if (!isset($rextra['recovery'])) { + // backwards compatibility check + $rextra['recovery'] = true; + } + + $chk = dbFetchRow('SELECT alerts.alerted,devices.ignore,devices.disabled FROM alerts,devices WHERE alerts.device_id = ? && devices.device_id = alerts.device_id && alerts.rule_id = ?', array($alert['device_id'], $alert['rule_id'])); + + if ($chk['alerted'] == $alert['state']) { + $noiss = true; + } + + $tolerence_window = Config::get('alert.tolerance_window'); + if (!empty($rextra['count']) && empty($rextra['interval'])) { + // This check below is for compat-reasons + if (!empty($rextra['delay'])) { + if ((time() - strtotime($alert['time_logged']) + $tolerence_window) < $rextra['delay'] || (!empty($alert['details']['delay']) && (time() - $alert['details']['delay'] + $tolerence_window) < $rextra['delay'])) { + continue; + } else { + $alert['details']['delay'] = time(); + $updet = true; + } + } + + if ($alert['state'] == 1 && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) { + if ($alert['details']['count'] < $rextra['count']) { + $noacc = true; + } + + $updet = true; + $noiss = false; + } + } else { + // This is the new way + if (!empty($rextra['delay']) && (time() - strtotime($alert['time_logged']) + $tolerence_window) < $rextra['delay']) { + continue; + } + + if (!empty($rextra['interval'])) { + if (!empty($alert['details']['interval']) && (time() - $alert['details']['interval'] + $tolerence_window) < $rextra['interval']) { + continue; + } else { + $alert['details']['interval'] = time(); + $updet = true; + } + } + + if (in_array($alert['state'], [1,3,4]) && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) { + if ($alert['details']['count'] < $rextra['count']) { + $noacc = true; + } + + $updet = true; + $noiss = false; + } + } + if ($chk['ignore'] == 1 || $chk['disabled'] == 1) { + $noiss = true; + $updet = false; + $noacc = false; + } + + if (AlertUtil::IsMaintenance($alert['device_id']) > 0) { + $noiss = true; + $noacc = true; + } + + if ($updet) { + dbUpdate(array('details' => gzcompress(json_encode($alert['details']), 9)), 'alert_log', 'id = ?', array($alert['id'])); + } + + if (!empty($rextra['mute'])) { + echo 'Muted Alert-UID #'.$alert['id']."\r\n"; + $noiss = true; + } + + if ($this->isParentDown($alert['device_id'])) { + $noiss = true; + Log::event('Skipped alerts because all parent devices are down', $alert['device_id'], 'alert', 1); + } + + if ($alert['state'] == 0 && $rextra['recovery'] == false) { + // Rule is set to not send a recovery alert + $noiss = true; + } + + if (!$noiss) { + $this->issueAlert($alert); + dbUpdate(array('alerted' => $alert['state']), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); + } + + if (!$noacc) { + dbUpdate(array('open' => 0), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); + } + } + } + + + /** + * Run external transports + * @param array $obj Alert-Array + * @return void + */ + public function extTransports($obj) + { + $type = new Template; + + // If alert transport mapping exists, override the default transports + $transport_maps = AlertUtil::getAlertTransports($obj['alert_id']); + + if (!$transport_maps) { + $transport_maps = AlertUtil::getDefaultAlertTransports(); + } + + // alerting for default contacts, etc + if (Config::get('alert.transports.mail') === true && !empty($obj['contacts'])) { + $transport_maps[] = [ + 'transport_id' => null, + 'transport_type' => 'mail', + 'opts' => $obj, + ]; + } + + foreach ($transport_maps as $item) { + $class = 'LibreNMS\\Alert\\Transport\\'.ucfirst($item['transport_type']); + if (class_exists($class)) { + //FIXME remove Deprecated transport + $transport_title = "Transport {$item['transport_type']}"; + $obj['transport'] = $item['transport_type']; + $obj['transport_name'] = $item['transport_name']; + $obj['alert'] = new AlertData($obj); + $obj['title'] = $type->getTitle($obj); + $obj['alert']['title'] = $obj['title']; + $obj['msg'] = $type->getBody($obj); + c_echo(" :: $transport_title => "); + $instance = new $class($item['transport_id']); + $tmp = $instance->deliverAlert($obj, $item['opts']); + $this->alertLog($tmp, $obj, $obj['transport']); + unset($instance); + echo PHP_EOL; + } + } + + if (count($transport_maps) === 0) { + echo 'No configured transports'; + } + } + + // Log alert event + public function alertLog($result, $obj, $transport) + { + $prefix = [ + 0 => "recovery", + 1 => $obj['severity']." alert", + 2 => "acknowledgment" + ]; + $prefix[3] = &$prefix[0]; + $prefix[4] = &$prefix[0]; + if ($result === true) { + echo 'OK'; + Log::event('Issued ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], 'alert', 1); + } elseif ($result === false) { + echo 'ERROR'; + Log::event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], null, 5); + } else { + echo "ERROR: $result\r\n"; + Log::event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "' Error: " . $result, $obj['device_id'], 'error', 5); + } + return; + } + + /** + * Check if a device's all parent are down + * Returns true if all parents are down + * @param int $device Device-ID + * @return bool + */ + public function isParentDown($device) + { + $parent_count = dbFetchCell("SELECT count(*) from `device_relationships` WHERE `child_device_id` = ?", array($device)); + if (!$parent_count) { + return false; + } + + + $down_parent_count = dbFetchCell("SELECT count(*) from devices as d LEFT JOIN devices_attribs as a ON d.device_id=a.device_id LEFT JOIN device_relationships as r ON d.device_id=r.parent_device_id WHERE d.status=0 AND d.ignore=0 AND d.disabled=0 AND r.child_device_id=? AND (d.status_reason='icmp' OR (a.attrib_type='override_icmp_disable' AND a.attrib_value=true))", array($device)); + if ($down_parent_count == $parent_count) { + return true; + } + + return false; + } +} diff --git a/LibreNMS/Snmptrap/Dispatcher.php b/LibreNMS/Snmptrap/Dispatcher.php index 2975b5f8ff..fea5c21d32 100644 --- a/LibreNMS/Snmptrap/Dispatcher.php +++ b/LibreNMS/Snmptrap/Dispatcher.php @@ -27,6 +27,7 @@ namespace LibreNMS\Snmptrap; use LibreNMS\Config; use LibreNMS\Snmptrap\Handlers\Fallback; +use LibreNMS\Alert\AlertRules; use Log; class Dispatcher @@ -52,6 +53,9 @@ class Dispatcher $logging = Config::get('snmptraps.eventlog', 'unhandled'); if ($logging == 'all' || ($fallback && $logging == 'unhandled')) { Log::event("SNMP trap received: " . $trap->getTrapOid(), $trap->getDevice(), 'trap'); + } else { + $rules = new AlertRules; + $rules->runRules($trap->getDevice()->device_id); } return !$fallback; diff --git a/alerts.php b/alerts.php index c3ee9c51aa..9106c28842 100755 --- a/alerts.php +++ b/alerts.php @@ -14,26 +14,31 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - */ - -/** + * * Alerts Cronjob * @author f0o * @copyright 2014 f0o, LibreNMS * @license GPL * @package LibreNMS * @subpackage Alerts + + * Edited 4/1/19 + * Changed to OOP + * @author: Heath Barnhart */ use LibreNMS\Util\FileLock; +use LibreNMS\Alert\RunAlerts; -$init_modules = ['alerts', 'laravel']; +$init_modules = ['alerts','laravel']; require __DIR__ . '/includes/init.php'; $options = getopt('d::'); $alerts_lock = FileLock::lockOrDie('alerts'); +$alerts = new RunAlerts(); + if (set_debug(isset($options['d']))) { echo "DEBUG!\n"; } @@ -41,13 +46,13 @@ if (set_debug(isset($options['d']))) { if (!defined('TEST') && \LibreNMS\Config::get('alert.disable') != 'true') { echo 'Start: '.date('r')."\r\n"; echo "ClearStaleAlerts():" . PHP_EOL; - ClearStaleAlerts(); + $alerts->clearStaleAlerts(); echo "RunFollowUp():\r\n"; - RunFollowUp(); + $alerts->runFollowUp(); echo "RunAlerts():\r\n"; - RunAlerts(); + $alerts->runAlerts(); echo "RunAcks():\r\n"; - RunAcks(); + $alerts->runAcks(); echo 'End : '.date('r')."\r\n"; } diff --git a/app/Jobs/PingCheck.php b/app/Jobs/PingCheck.php index 4ed81ba47d..1afbf7c4c7 100644 --- a/app/Jobs/PingCheck.php +++ b/app/Jobs/PingCheck.php @@ -37,6 +37,8 @@ use Illuminate\Support\Collection; use LibreNMS\Config; use LibreNMS\RRD\RrdDefinition; use Symfony\Component\Process\Process; +use LibreNMS\Alert\AlertRules; +use Log; class PingCheck implements ShouldQueue { @@ -248,13 +250,15 @@ class PingCheck implements ShouldQueue // if changed, update reason $device->status_reason = $device->status ? '' : 'icmp'; $type = $device->status ? 'up' : 'down'; - log_event('Device status changed to ' . ucfirst($type) . " from icmp check.", $device->toArray(), $type); - - echo "Device $device->hostname changed status to $type, running alerts\n"; - RunRules($device->device_id); + Log::event('Device status changed to ' . ucfirst($type) . " from icmp check.", $device->device_id, $type); } + $device->save(); // only saves if needed (which is every time because of last_ping) + echo "Device $device->hostname changed status to $type, running alerts\n"; + $rules = new AlertRules; + $rules->runRules($device->device_id); + // add data to rrd data_update($device->toArray(), 'ping-perf', $this->rrd_tags, ['ping' => $device->last_ping_timetaken]); diff --git a/includes/alerts.inc.php b/includes/alerts.inc.php deleted file mode 100644 index f8f129e2b3..0000000000 --- a/includes/alerts.inc.php +++ /dev/null @@ -1,898 +0,0 @@ - - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . */ - -/* - * Alerts Tracking - * @author Daniel Preussker - * @copyright 2014 f0o, LibreNMS - * @license GPL - * @package LibreNMS - * @subpackage Alerts - */ - -use App\Models\DevicePerf; -use LibreNMS\Alert\Template; -use LibreNMS\Alert\AlertData; -use LibreNMS\Alerting\QueryBuilderParser; -use LibreNMS\Authentication\LegacyAuth; -use LibreNMS\Alert\AlertUtil; -use LibreNMS\Config; -use PHPMailer\PHPMailer\PHPMailer; -use LibreNMS\Util\Time; - -/** - * @param $rule - * @param $query_builder - * @return bool|string - */ -function GenSQL($rule, $query_builder = false) -{ - if ($query_builder) { - return QueryBuilderParser::fromJson($query_builder)->toSql(); - } else { - return GenSQLOld($rule); - } -} - -/** - * Generate SQL from Rule - * @param string $rule Rule to generate SQL for - * @return string|boolean - */ -function GenSQLOld($rule) -{ - $rule = RunMacros($rule); - if (empty($rule)) { - //Cannot resolve Macros due to recursion. Rule is invalid. - return false; - } - //Pretty-print rule to dissect easier - $pretty = array('&&' => ' && ', '||' => ' || '); - $rule = str_replace(array_keys($pretty), $pretty, $rule); - $tmp = explode(" ", $rule); - $tables = array(); - foreach ($tmp as $opt) { - if (strstr($opt, '%') && strstr($opt, '.')) { - $tmpp = explode(".", $opt, 2); - $tmpp[0] = str_replace("%", "", $tmpp[0]); - $tables[] = mres(str_replace("(", "", $tmpp[0])); - $rule = str_replace($opt, $tmpp[0].'.'.$tmpp[1], $rule); - } - } - $tables = array_keys(array_flip($tables)); - if (dbFetchCell('SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_NAME = ? && COLUMN_NAME = ?', array($tables[0],'device_id')) != 1) { - //Our first table has no valid glue, append the 'devices' table to it! - array_unshift($tables, 'devices'); - } - $x = sizeof($tables)-1; - $i = 0; - $join = ""; - while ($i < $x) { - if (isset($tables[$i+1])) { - $gtmp = ResolveGlues(array($tables[$i+1]), 'device_id'); - if ($gtmp === false) { - //Cannot resolve glue-chain. Rule is invalid. - return false; - } - $last = ""; - $qry = ""; - foreach ($gtmp as $glue) { - if (empty($last)) { - list($tmp,$last) = explode('.', $glue); - $qry .= $glue.' = '; - } else { - list($tmp,$new) = explode('.', $glue); - $qry .= $tmp.'.'.$last.' && '.$tmp.'.'.$new.' = '; - $last = $new; - } - if (!in_array($tmp, $tables)) { - $tables[] = $tmp; - } - } - $join .= "( ".$qry.$tables[0].".device_id ) && "; - } - $i++; - } - $sql = "SELECT * FROM ".implode(",", $tables)." WHERE (".$join."".str_replace("(", "", $tables[0]).".device_id = ?) && (".str_replace(array("%","@","!~","~"), array("",".*","NOT REGEXP","REGEXP"), $rule).")"; - return $sql; -} - -/** - * Process Macros - * @param string $rule Rule to process - * @param int $x Recursion-Anchor - * @return string|boolean - */ -function RunMacros($rule, $x = 1) -{ - $macros = Config::get('alert.macros.rule', []) . - krsort($macros); - foreach ($macros as $macro => $value) { - if (!strstr($macro, " ")) { - $rule = str_replace('%macros.'.$macro, '('.$value.')', $rule); - } - } - if (strstr($rule, "%macros.")) { - if (++$x < 30) { - $rule = RunMacros($rule, $x); - } else { - return false; - } - } - return $rule; -} - -/** - * Get Alert-Rules for Devices - * @param int $device_id Device-ID - * @return array - */ -function GetRules($device_id) -{ - $query = "SELECT DISTINCT a.* FROM alert_rules a - LEFT JOIN alert_device_map d ON a.id=d.rule_id - LEFT JOIN alert_group_map g ON a.id=g.rule_id - LEFT JOIN device_group_device dg ON g.group_id=dg.device_group_id - WHERE a.disabled = 0 AND ((d.device_id IS NULL AND g.group_id IS NULL) OR d.device_id=? OR dg.device_id=?)"; - - $params = [$device_id, $device_id]; - return dbFetchRows($query, $params); -} - -/** - * Check if device is under maintenance - * @param int $device_id Device-ID - * @return bool - */ -function IsMaintenance($device_id) -{ - $device = \App\Models\Device::find($device_id); - return !is_null($device) && $device->isUnderMaintenance(); -} -/** - * Run all rules for a device - * @param int $device_id Device-ID - * @return void - */ -function RunRules($device_id) -{ - if (IsMaintenance($device_id) > 0) { - echo "Under Maintenance, Skipping alerts.\r\n"; - return false; - } - foreach (GetRules($device_id) as $rule) { - c_echo('Rule %p#'.$rule['id'].' (' . $rule['name'] . '):%n '); - $extra = json_decode($rule['extra'], true); - if (isset($extra['invert'])) { - $inv = (bool) $extra['invert']; - } else { - $inv = false; - } - d_echo(PHP_EOL); - if (empty($rule['query'])) { - $rule['query'] = GenSQL($rule['rule'], $rule['builder']); - } - $sql = $rule['query']; - $qry = dbFetchRows($sql, array($device_id)); - $cnt = count($qry); - for ($i = 0; $i < $cnt; $i++) { - if (isset($qry[$i]['ip'])) { - $qry[$i]['ip'] = inet6_ntop($qry[$i]['ip']); - } - } - $s = sizeof($qry); - if ($s == 0 && $inv === false) { - $doalert = false; - } elseif ($s > 0 && $inv === false) { - $doalert = true; - } elseif ($s == 0 && $inv === true) { - $doalert = true; - } else { //( $s > 0 && $inv == false ) { - $doalert = false; - } - - $current_state = dbFetchCell("SELECT state FROM alerts WHERE rule_id = ? AND device_id = ? ORDER BY id DESC LIMIT 1", [$rule['id'], $device_id]); - if ($doalert) { - if ($current_state == 2) { - c_echo('Status: %ySKIP'); - } elseif ($current_state >= 1) { - c_echo('Status: %bNOCHG'); - // NOCHG here doesn't mean no change full stop. It means no change to the alert state - // So we update the details column with any fresh changes to the alert output we might have. - $alert_log = dbFetchRow('SELECT alert_log.id, alert_log.details FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0 ORDER BY alert_log.id DESC LIMIT 1', array($device_id, $rule['id'])); - $details = []; - if (!empty($alert_log['details'])) { - $details = json_decode(gzuncompress($alert_log['details']), true); - } - $details['contacts'] = GetContacts($qry); - $details['rule'] = $qry; - $details = gzcompress(json_encode($details), 9); - dbUpdate(array('details' => $details), 'alert_log', 'id = ?', array($alert_log['id'])); - } else { - $extra = gzcompress(json_encode(array('contacts' => GetContacts($qry), 'rule'=>$qry)), 9); - if (dbInsert(['state' => 1, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'details' => $extra], 'alert_log')) { - if (is_null($current_state)) { - dbInsert(array('state' => 1, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'open' => 1,'alerted' => 0), 'alerts'); - } else { - dbUpdate(['state' => 1, 'open' => 1], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]); - } - c_echo(PHP_EOL . 'Status: %rALERT'); - } - } - } else { - if (!is_null($current_state) && $current_state == 0) { - c_echo('Status: %bNOCHG'); - } else { - if (dbInsert(['state' => 0, 'device_id' => $device_id, 'rule_id' => $rule['id']], 'alert_log')) { - if (is_null($current_state)) { - dbInsert(['state' => 0, 'device_id' => $device_id, 'rule_id' => $rule['id'], 'open' => 1, 'alerted' => 0], 'alerts'); - } else { - dbUpdate(['state' => 0, 'open' => 1, 'note' => ''], 'alerts', 'device_id = ? && rule_id = ?', [$device_id, $rule['id']]); - } - - c_echo(PHP_EOL . 'Status: %gOK'); - } - } - } - c_echo('%n' . PHP_EOL); - } -} - -/** - * Find contacts for alert - * @param array $results Rule-Result - * @return array - */ -function GetContacts($results) -{ - if (empty($results)) { - return []; - } - if (Config::get('alert.default_only') === true || Config::get('alerts.email.default_only') === true) { - $email = Config::get('alert.default_mail', Config::get('alerts.email.default')); - return $email ? [$email => ''] : []; - } - $users = LegacyAuth::get()->getUserlist(); - $contacts = array(); - $uids = array(); - foreach ($results as $result) { - $tmp = null; - if (is_numeric($result["bill_id"])) { - $tmpa = dbFetchRows("SELECT user_id FROM bill_perms WHERE bill_id = ?", array($result["bill_id"])); - foreach ($tmpa as $tmp) { - $uids[$tmp['user_id']] = $tmp['user_id']; - } - } - if (is_numeric($result["port_id"])) { - $tmpa = dbFetchRows("SELECT user_id FROM ports_perms WHERE port_id = ?", array($result["port_id"])); - foreach ($tmpa as $tmp) { - $uids[$tmp['user_id']] = $tmp['user_id']; - } - } - if (is_numeric($result["device_id"])) { - if (Config::get('alert.syscontact') == true) { - if (dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_bool' AND device_id = ?", [$result["device_id"]])) { - $tmpa = dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_string' AND device_id = ?", array($result["device_id"])); - } else { - $tmpa = dbFetchCell("SELECT sysContact FROM devices WHERE device_id = ?", array($result["device_id"])); - } - if (!empty($tmpa)) { - $contacts[$tmpa] = ''; - } - } - $tmpa = dbFetchRows("SELECT user_id FROM devices_perms WHERE device_id = ?", array($result["device_id"])); - foreach ($tmpa as $tmp) { - $uids[$tmp['user_id']] = $tmp['user_id']; - } - } - } - foreach ($users as $user) { - if (empty($user['email'])) { - continue; // no email, skip this user - } - if (empty($user['realname'])) { - $user['realname'] = $user['username']; - } - if (empty($user['level'])) { - $user['level'] = LegacyAuth::get()->getUserlevel($user['username']); - } - if (Config::get('alert.globals') && ($user['level'] >= 5 && $user['level'] < 10)) { - $contacts[$user['email']] = $user['realname']; - } elseif (Config::get('alert.admins') && $user['level'] == 10) { - $contacts[$user['email']] = $user['realname']; - } elseif (Config::get('alert.users') == true && in_array($user['user_id'], $uids)) { - $contacts[$user['email']] = $user['realname']; - } - } - - $tmp_contacts = array(); - foreach ($contacts as $email => $name) { - if (strstr($email, ',')) { - $split_contacts = preg_split('/[,\s]+/', $email); - foreach ($split_contacts as $split_email) { - if (!empty($split_email)) { - $tmp_contacts[$split_email] = $name; - } - } - } else { - $tmp_contacts[$email] = $name; - } - } - - if (!empty($tmp_contacts)) { - // Validate contacts so we can fall back to default if configured. - $mail = new PHPMailer(); - foreach ($tmp_contacts as $tmp_email => $tmp_name) { - if ($mail->validateAddress($tmp_email) != true) { - unset($tmp_contacts[$tmp_email]); - } - } - } - - # Copy all email alerts to default contact if configured. - if (!isset($tmp_contacts[Config::get('alert.default_mail')]) && (Config::get('alert.default_copy'))) { - $tmp_contacts[Config::get('alert.default_mail')] = ''; - } - - # Send email to default contact if no other contact found - if ((count($tmp_contacts) == 0) && (Config::get('alert.default_if_none')) && (!empty(Config::get('alert.default_mail')))) { - $tmp_contacts[Config::get('alert.default_mail')] = ''; - } - - return $tmp_contacts; -} - -/** - * Populate variables - * @param string $txt Text with variables - * @param boolean $wrap Wrap variable for text-usage (default: true) - * @return string - */ -function populate($txt, $wrap = true) -{ - preg_match_all('/%([\w\.]+)/', $txt, $m); - foreach ($m[1] as $tmp) { - $orig = $tmp; - $rep = false; - if ($tmp == 'key' || $tmp == 'value') { - $rep = '$'.$tmp; - } else { - if (strstr($tmp, '.')) { - $tmp = explode('.', $tmp, 2); - $pre = '$'.$tmp[0]; - $tmp = $tmp[1]; - } else { - $pre = '$obj'; - } - - $rep = $pre."['".str_replace('.', "']['", $tmp)."']"; - if ($wrap) { - $rep = '{'.$rep.'}'; - } - } - - $txt = str_replace('%'.$orig, $rep, $txt); - }//end foreach - return $txt; -}//end populate() - -/** - * Describe Alert - * @param array $alert Alert-Result from DB - * @return array|boolean - */ -function DescribeAlert($alert) -{ - $obj = array(); - $i = 0; - $device = dbFetchRow('SELECT hostname, sysName, sysDescr, sysContact, os, type, ip, hardware, version, purpose, notes, uptime, status, status_reason, locations.location FROM devices LEFT JOIN locations ON locations.id = devices.location_id WHERE device_id = ?', array($alert['device_id'])); - $attribs = get_dev_attribs($alert['device_id']); - - $obj['hostname'] = $device['hostname']; - $obj['sysName'] = $device['sysName']; - $obj['sysDescr'] = $device['sysDescr']; - $obj['sysContact'] = $device['sysContact']; - $obj['os'] = $device['os']; - $obj['type'] = $device['type']; - $obj['ip'] = inet6_ntop($device['ip']); - $obj['hardware'] = $device['hardware']; - $obj['version'] = $device['version']; - $obj['location'] = $device['location']; - $obj['uptime'] = $device['uptime']; - $obj['uptime_short'] = Time::formatInterval($device['uptime'], 'short'); - $obj['uptime_long'] = Time::formatInterval($device['uptime']); - $obj['description'] = $device['purpose']; - $obj['notes'] = $device['notes']; - $obj['alert_notes'] = $alert['note']; - $obj['device_id'] = $alert['device_id']; - $obj['rule_id'] = $alert['rule_id']; - $obj['status'] = $device['status']; - $obj['status_reason'] = $device['status_reason']; - if (can_ping_device($attribs)) { - $ping_stats = DevicePerf::where('device_id', $alert['device_id'])->latest('timestamp')->first(); - $obj['ping_timestamp'] = $ping_stats->template; - $obj['ping_loss'] = $ping_stats->loss; - $obj['ping_min'] = $ping_stats->min; - $obj['ping_max'] = $ping_stats->max; - $obj['ping_avg'] = $ping_stats->avg; - $obj['debug'] = json_decode($ping_stats->debug, true); - } - $extra = $alert['details']; - - $tpl = new Template; - $template = $tpl->getTemplate($obj); - - if ($alert['state'] >= 1) { - $obj['title'] = $template->title ?: 'Alert for device '.$device['hostname'].' - '.($alert['name'] ? $alert['name'] : $alert['rule']); - if ($alert['state'] == 2) { - $obj['title'] .= ' got acknowledged'; - } elseif ($alert['state'] == 3) { - $obj['title'] .= ' got worse'; - } elseif ($alert['state'] == 4) { - $obj['title'] .= ' got better'; - } - - foreach ($extra['rule'] as $incident) { - $i++; - $obj['faults'][$i] = $incident; - $obj['faults'][$i]['string'] = null; - foreach ($incident as $k => $v) { - if (!empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) { - $obj['faults'][$i]['string'] .= $k.' = '.$v.'; '; - } - } - } - $obj['elapsed'] = TimeFormat(time() - strtotime($alert['time_logged'])); - if (!empty($extra['diff'])) { - $obj['diff'] = $extra['diff']; - } - } elseif ($alert['state'] == 0) { - // Alert is now cleared - $id = dbFetchRow('SELECT alert_log.id,alert_log.time_logged,alert_log.details FROM alert_log WHERE alert_log.state != 2 && alert_log.state != 0 && alert_log.rule_id = ? && alert_log.device_id = ? && alert_log.id < ? ORDER BY id DESC LIMIT 1', array($alert['rule_id'], $alert['device_id'], $alert['id'])); - if (empty($id['id'])) { - return false; - } - - $extra = []; - if (!empty($id['details'])) { - $extra = json_decode(gzuncompress($id['details']), true); - } - - // Reset count to 0 so alerts will continue - $extra['count'] = 0; - dbUpdate(array('details' => gzcompress(json_encode($id['details']), 9)), 'alert_log', 'id = ?', array($alert['id'])); - - $obj['title'] = $template->title_rec ?: 'Device '.$device['hostname'].' recovered from '.($alert['name'] ? $alert['name'] : $alert['rule']); - $obj['elapsed'] = TimeFormat(strtotime($alert['time_logged']) - strtotime($id['time_logged'])); - $obj['id'] = $id['id']; - foreach ($extra['rule'] as $incident) { - $i++; - $obj['faults'][$i] = $incident; - foreach ($incident as $k => $v) { - if (!empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) { - $obj['faults'][$i]['string'] .= $k.' => '.$v.'; '; - } - } - } - } else { - return 'Unknown State'; - }//end if - $obj['builder'] = $alert['builder']; - $obj['uid'] = $alert['id']; - $obj['alert_id'] = $alert['alert_id']; - $obj['severity'] = $alert['severity']; - $obj['rule'] = $alert['rule']; - $obj['name'] = $alert['name']; - $obj['timestamp'] = $alert['time_logged']; - $obj['contacts'] = $extra['contacts']; - $obj['state'] = $alert['state']; - $obj['template'] = $template; - return $obj; -}//end DescribeAlert() - -/** - * Format Elapsed Time - * @param integer $secs Seconds elapsed - * @return string - */ -function TimeFormat($secs) -{ - $bit = array( - 'y' => $secs / 31556926 % 12, - 'w' => $secs / 604800 % 52, - 'd' => $secs / 86400 % 7, - 'h' => $secs / 3600 % 24, - 'm' => $secs / 60 % 60, - 's' => $secs % 60, - ); - $ret = array(); - foreach ($bit as $k => $v) { - if ($v > 0) { - $ret[] = $v.$k; - } - } - - if (empty($ret)) { - return 'none'; - } - - return join(' ', $ret); -}//end TimeFormat() - - -function ClearStaleAlerts() -{ - $sql = "SELECT `alerts`.`id` AS `alert_id`, `devices`.`hostname` AS `hostname` FROM `alerts` LEFT JOIN `devices` ON `alerts`.`device_id`=`devices`.`device_id` RIGHT JOIN `alert_rules` ON `alerts`.`rule_id`=`alert_rules`.`id` WHERE `alerts`.`state`!=0 AND `devices`.`hostname` IS NULL"; - foreach (dbFetchRows($sql) as $alert) { - if (empty($alert['hostname']) && isset($alert['alert_id'])) { - dbDelete('alerts', '`id` = ?', array($alert['alert_id'])); - echo "Stale-alert: #{$alert['alert_id']}" . PHP_EOL; - } - } -} - -/** - * Re-Validate Rule-Mappings - * @param integer $device_id Device-ID - * @param integer $rule Rule-ID - * @return boolean - */ -function IsRuleValid($device_id, $rule) -{ - global $rulescache; - if (empty($rulescache[$device_id]) || !isset($rulescache[$device_id])) { - foreach (GetRules($device_id) as $chk) { - $rulescache[$device_id][$chk['id']] = true; - } - } - - if ($rulescache[$device_id][$rule] === true) { - return true; - } - - return false; -}//end IsRuleValid() - - -/** - * Issue Alert-Object - * @param array $alert - * @return boolean - */ -function IssueAlert($alert) -{ - if (dbFetchCell('SELECT attrib_value FROM devices_attribs WHERE attrib_type = "disable_notify" && device_id = ?', array($alert['device_id'])) == '1') { - return true; - } - - if (Config::get('alert.fixed-contacts') == false) { - if (empty($alert['query'])) { - $alert['query'] = GenSQL($alert['rule'], $alert['builder']); - } - $sql = $alert['query']; - $qry = dbFetchRows($sql, array($alert['device_id'])); - $alert['details']['contacts'] = GetContacts($qry); - } - - $obj = DescribeAlert($alert); - if (is_array($obj)) { - echo 'Issuing Alert-UID #'.$alert['id'].'/'.$alert['state'].':' . PHP_EOL; - ExtTransports($obj); - - echo "\r\n"; - } - - return true; -}//end IssueAlert() - - -/** - * Issue ACK notification - * @return void - */ -function RunAcks() -{ - - foreach (loadAlerts('alerts.state = 2 && alerts.open = 1') as $alert) { - IssueAlert($alert); - dbUpdate(array('open' => 0), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); - } -}//end RunAcks() - -/** - * Run Follow-Up alerts - * @return void - */ -function RunFollowUp() -{ - foreach (loadAlerts('alerts.state > 0 && alerts.open = 0') as $alert) { - if ($alert['state'] != 2 || ($alert['info']['until_clear'] === false)) { - $rextra = json_decode($alert['extra'], true); - if ($rextra['invert']) { - continue; - } - - if (empty($alert['query'])) { - $alert['query'] = GenSQL($alert['rule'], $alert['builder']); - } - $chk = dbFetchRows($alert['query'], array($alert['device_id'])); - //make sure we can json_encode all the datas later - $cnt = count($chk); - for ($i = 0; $i < $cnt; $i++) { - if (isset($chk[$i]['ip'])) { - $chk[$i]['ip'] = inet6_ntop($chk[$i]['ip']); - } - } - $o = sizeof($alert['details']['rule']); - $n = sizeof($chk); - $ret = 'Alert #' . $alert['id']; - $state = 0; - if ($n > $o) { - $ret .= ' Worsens'; - $state = 3; - $alert['details']['diff'] = array_diff($chk, $alert['details']['rule']); - } elseif ($n < $o) { - $ret .= ' Betters'; - $state = 4; - $alert['details']['diff'] = array_diff($alert['details']['rule'], $chk); - } - - if ($state > 0 && $n > 0) { - $alert['details']['rule'] = $chk; - if (dbInsert(array( - 'state' => $state, - 'device_id' => $alert['device_id'], - 'rule_id' => $alert['rule_id'], - 'details' => gzcompress(json_encode($alert['details']), 9) - ), 'alert_log')) { - dbUpdate(array('state' => $state, 'open' => 1, 'alerted' => 1), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); - } - - echo $ret . ' (' . $o . '/' . $n . ")\r\n"; - } - } - }//end foreach -}//end RunFollowUp() - -function loadAlerts($where) -{ - $alerts = []; - foreach (dbFetchRows("SELECT alerts.id, alerts.device_id, alerts.rule_id, alerts.state, alerts.note, alerts.info FROM alerts WHERE $where") as $alert_status) { - $alert = dbFetchRow( - 'SELECT alert_log.id,alert_log.rule_id,alert_log.device_id,alert_log.state,alert_log.details,alert_log.time_logged,alert_rules.rule,alert_rules.severity,alert_rules.extra,alert_rules.name,alert_rules.query,alert_rules.builder FROM alert_log,alert_rules WHERE alert_log.rule_id = alert_rules.id && alert_log.device_id = ? && alert_log.rule_id = ? && alert_rules.disabled = 0 ORDER BY alert_log.id DESC LIMIT 1', - array($alert_status['device_id'], $alert_status['rule_id']) - ); - - if (empty($alert['rule_id']) || !IsRuleValid($alert_status['device_id'], $alert_status['rule_id'])) { - echo 'Stale-Rule: #' . $alert_status['rule_id'] . '/' . $alert_status['device_id'] . "\r\n"; - // Alert-Rule does not exist anymore, let's remove the alert-state. - dbDelete('alerts', 'rule_id = ? && device_id = ?', [$alert_status['rule_id'], $alert_status['device_id']]); - } else { - $alert['alert_id'] = $alert_status['id']; - $alert['state'] = $alert_status['state']; - $alert['note'] = $alert_status['note']; - if (!empty($alert['details'])) { - $alert['details'] = json_decode(gzuncompress($alert['details']), true); - } - $alert['info'] = json_decode($alert_status['info'], true); - $alerts[] = $alert; - } - } - - return $alerts; -} - -/** - * Run all alerts - * @return void - */ -function RunAlerts() -{ - foreach (loadAlerts('alerts.state != 2 && alerts.open = 1') as $alert) { - $noiss = false; - $noacc = false; - $updet = false; - $rextra = json_decode($alert['extra'], true); - if (!isset($rextra['recovery'])) { - // backwards compatibility check - $rextra['recovery'] = true; - } - - $chk = dbFetchRow('SELECT alerts.alerted,devices.ignore,devices.disabled FROM alerts,devices WHERE alerts.device_id = ? && devices.device_id = alerts.device_id && alerts.rule_id = ?', array($alert['device_id'], $alert['rule_id'])); - - if ($chk['alerted'] == $alert['state']) { - $noiss = true; - } - - if (!empty($rextra['count']) && empty($rextra['interval'])) { - // This check below is for compat-reasons - if (!empty($rextra['delay'])) { - if ((time() - strtotime($alert['time_logged']) + Config::get('alert.tolerance_window')) < $rextra['delay'] || (!empty($alert['details']['delay']) && (time() - $alert['details']['delay'] + Config::get('alert.tolerance_window')) < $rextra['delay'])) { - continue; - } else { - $alert['details']['delay'] = time(); - $updet = true; - } - } - - if ($alert['state'] == 1 && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) { - if ($alert['details']['count'] < $rextra['count']) { - $noacc = true; - } - - $updet = true; - $noiss = false; - } - } else { - // This is the new way - if (!empty($rextra['delay']) && (time() - strtotime($alert['time_logged']) + Config::get('alert.tolerance_window')) < $rextra['delay']) { - continue; - } - - if (!empty($rextra['interval'])) { - if (!empty($alert['details']['interval']) && (time() - $alert['details']['interval'] + Config::get('alert.tolerance_window')) < $rextra['interval']) { - continue; - } else { - $alert['details']['interval'] = time(); - $updet = true; - } - } - - if (in_array($alert['state'], [1,3,4]) && !empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) { - if ($alert['details']['count'] < $rextra['count']) { - $noacc = true; - } - - $updet = true; - $noiss = false; - } - }//end if - if ($chk['ignore'] == 1 || $chk['disabled'] == 1) { - $noiss = true; - $updet = false; - $noacc = false; - } - - if (IsMaintenance($alert['device_id']) > 0) { - $noiss = true; - $noacc = true; - } - - if ($updet) { - dbUpdate(array('details' => gzcompress(json_encode($alert['details']), 9)), 'alert_log', 'id = ?', array($alert['id'])); - } - - if (!empty($rextra['mute'])) { - echo 'Muted Alert-UID #'.$alert['id']."\r\n"; - $noiss = true; - } - - if (IsParentDown($alert['device_id'])) { - $noiss = true; - log_event('Skipped alerts because all parent devices are down', $alert['device_id'], 'alert', 1); - } - - if ($alert['state'] == 0 && $rextra['recovery'] == false) { - // Rule is set to not send a recovery alert - $noiss = true; - } - - if (!$noiss) { - IssueAlert($alert); - dbUpdate(array('alerted' => $alert['state']), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); - } - - if (!$noacc) { - dbUpdate(array('open' => 0), 'alerts', 'rule_id = ? && device_id = ?', array($alert['rule_id'], $alert['device_id'])); - } - }//end foreach -}//end RunAlerts() - - -/** - * Run external transports - * @param array $obj Alert-Array - * @return void - */ -function ExtTransports($obj) -{ - $type = new Template; - - // If alert transport mapping exists, override the default transports - $transport_maps = AlertUtil::getAlertTransports($obj['alert_id']); - - if (!$transport_maps) { - $transport_maps = AlertUtil::getDefaultAlertTransports(); - } - - // alerting for default contacts, etc - if (Config::get('alert.transports.mail') === true && !empty($obj['contacts'])) { - $transport_maps[] = [ - 'transport_id' => null, - 'transport_type' => 'mail', - 'opts' => $obj, - ]; - } - - foreach ($transport_maps as $item) { - $class = 'LibreNMS\\Alert\\Transport\\'.ucfirst($item['transport_type']); - if (class_exists($class)) { - //FIXME remove Deprecated transport - $transport_title = "Transport {$item['transport_type']}"; - $obj['transport'] = $item['transport_type']; - $obj['transport_name'] = $item['transport_name']; - $obj['alert'] = new AlertData($obj); - $obj['title'] = $type->getTitle($obj); - $obj['alert']['title'] = $obj['title']; - $obj['msg'] = $type->getBody($obj); - c_echo(" :: $transport_title => "); - $instance = new $class($item['transport_id']); - $tmp = $instance->deliverAlert($obj, $item['opts']); - AlertLog($tmp, $obj, $obj['transport']); - unset($instance); - echo PHP_EOL; - } - } - - if (count($transport_maps) === 0) { - echo 'No configured transports'; - } -}//end ExtTransports() - -// Log alert event -function AlertLog($result, $obj, $transport) -{ - $prefix = [ - 0 => "recovery", - 1 => $obj['severity']." alert", - 2 => "acknowledgment" - ]; - $prefix[3] = &$prefix[0]; - $prefix[4] = &$prefix[0]; - if ($result === true) { - echo 'OK'; - log_event('Issued ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], 'alert', 1); - } elseif ($result === false) { - echo 'ERROR'; - log_event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], null, 5); - } else { - echo "ERROR: $result\r\n"; - log_event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "' Error: " . $result, $obj['device_id'], 'error', 5); - } - return; -}//end AlertLog() - - -/** - * Check if a device's all parent are down - * Returns true if all parents are down - * @param int $device Device-ID - * @return bool - */ -function IsParentDown($device) -{ - $parent_count = dbFetchCell("SELECT count(*) from `device_relationships` WHERE `child_device_id` = ?", array($device)); - if (!$parent_count) { - return false; - } - - - $down_parent_count = dbFetchCell("SELECT count(*) from devices as d LEFT JOIN devices_attribs as a ON d.device_id=a.device_id LEFT JOIN device_relationships as r ON d.device_id=r.parent_device_id WHERE d.status=0 AND d.ignore=0 AND d.disabled=0 AND r.child_device_id=? AND (d.status_reason='icmp' OR (a.attrib_type='override_icmp_disable' AND a.attrib_value=true))", array($device)); - if ($down_parent_count == $parent_count) { - return true; - } - - return false; -} //end IsParentDown() diff --git a/includes/init.php b/includes/init.php index d08da125da..0119e9b3a7 100644 --- a/includes/init.php +++ b/includes/init.php @@ -86,7 +86,7 @@ if (module_selected('polling', $init_modules)) { } if (module_selected('alerts', $init_modules)) { - require_once $install_dir . '/includes/alerts.inc.php'; + require_once $install_dir . '/LibreNMS/Alert/RunAlerts.php'; } // Boot Laravel diff --git a/poller.php b/poller.php index 40e4a80473..3041caec3f 100755 --- a/poller.php +++ b/poller.php @@ -1,17 +1,33 @@ #!/usr/bin/env php . * * @package LibreNMS * @subpackage poller * @copyright (C) 2006 - 2012 Adam Armstrong + + * Modified 4/17/19 + * @author Heath Barnhart */ use LibreNMS\Config; +use LibreNMS\Alert\AlertRules; $init_modules = ['polling', 'alerts', 'laravel']; require __DIR__ . '/includes/init.php'; @@ -159,7 +175,8 @@ foreach (dbFetch($query) as $device) { } echo "#### Start Alerts ####\n"; - RunRules($device['device_id']); + $rules = new AlertRules(); + $rules->runRules($device['device_id']); echo "#### End Alerts ####\r\n"; $polled_devices++; }