1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* RunAlerts.php |
4
|
|
|
* |
5
|
|
|
* Copyright (C) 2014 Daniel Preussker <[email protected]> |
6
|
|
|
* This program is free software: you can redistribute it and/or modify |
7
|
|
|
* it under the terms of the GNU General Public License as published by |
8
|
|
|
* the Free Software Foundation, either version 3 of the License, or |
9
|
|
|
* (at your option) any later version. |
10
|
|
|
* |
11
|
|
|
* This program is distributed in the hope that it will be useful, |
12
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
13
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the |
14
|
|
|
* GNU General Public License for more details. |
15
|
|
|
* |
16
|
|
|
* You should have received a copy of the GNU General Public License |
17
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>. |
18
|
|
|
* |
19
|
|
|
* Original Code: |
20
|
|
|
* @author Daniel Preussker <[email protected]> |
21
|
|
|
* @copyright 2014 f0o, LibreNMS |
22
|
|
|
* @license GPL |
23
|
|
|
* @package LibreNMS |
24
|
|
|
* @subpackage Alerts |
25
|
|
|
* |
26
|
|
|
* Modified by: |
27
|
|
|
* @author Heath Barnhart <[email protected]> |
28
|
|
|
* |
29
|
|
|
*/ |
30
|
|
|
|
31
|
|
|
namespace LibreNMS\Alert; |
32
|
|
|
|
33
|
|
|
use App\Models\DevicePerf; |
34
|
|
|
use LibreNMS\Config; |
35
|
|
|
use LibreNMS\Enum\Alert; |
36
|
|
|
use LibreNMS\Enum\AlertState; |
37
|
|
|
use LibreNMS\Util\Time; |
38
|
|
|
use Log; |
39
|
|
|
|
40
|
|
|
class RunAlerts |
41
|
|
|
{ |
42
|
|
|
/** |
43
|
|
|
* Populate variables |
44
|
|
|
* |
45
|
|
|
* @param string $txt Text with variables |
46
|
|
|
* @param bool $wrap Wrap variable for text-usage (default: true) |
47
|
|
|
* @return string |
48
|
|
|
*/ |
49
|
|
|
public function populate($txt, $wrap = true) |
50
|
|
|
{ |
51
|
|
|
preg_match_all('/%([\w\.]+)/', $txt, $m); |
52
|
|
|
foreach ($m[1] as $tmp) { |
53
|
|
|
$orig = $tmp; |
54
|
|
|
$rep = false; |
55
|
|
|
if ($tmp == 'key' || $tmp == 'value') { |
56
|
|
|
$rep = '$' . $tmp; |
57
|
|
|
} else { |
58
|
|
|
if (strstr($tmp, '.')) { |
59
|
|
|
$tmp = explode('.', $tmp, 2); |
60
|
|
|
$pre = '$' . $tmp[0]; |
61
|
|
|
$tmp = $tmp[1]; |
62
|
|
|
} else { |
63
|
|
|
$pre = '$obj'; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
$rep = $pre . "['" . str_replace('.', "']['", $tmp) . "']"; |
67
|
|
|
if ($wrap) { |
68
|
|
|
$rep = '{' . $rep . '}'; |
69
|
|
|
} |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
$txt = str_replace('%' . $orig, $rep, $txt); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
return $txt; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Describe Alert |
80
|
|
|
* |
81
|
|
|
* @param array $alert Alert-Result from DB |
82
|
|
|
* @return array|bool|string |
83
|
|
|
*/ |
84
|
|
|
public function describeAlert($alert) |
85
|
|
|
{ |
86
|
|
|
$obj = []; |
87
|
|
|
$i = 0; |
88
|
|
|
$device = dbFetchRow('SELECT hostname, sysName, sysDescr, sysContact, os, type, ip, hardware, version, serial, features, purpose, notes, uptime, status, status_reason, locations.location FROM devices LEFT JOIN locations ON locations.id = devices.location_id WHERE device_id = ?', [$alert['device_id']]); |
89
|
|
|
$attribs = get_dev_attribs($alert['device_id']); |
90
|
|
|
|
91
|
|
|
$obj['hostname'] = $device['hostname']; |
92
|
|
|
$obj['sysName'] = $device['sysName']; |
93
|
|
|
$obj['sysDescr'] = $device['sysDescr']; |
94
|
|
|
$obj['sysContact'] = $device['sysContact']; |
95
|
|
|
$obj['os'] = $device['os']; |
96
|
|
|
$obj['type'] = $device['type']; |
97
|
|
|
$obj['ip'] = inet6_ntop($device['ip']); |
98
|
|
|
$obj['hardware'] = $device['hardware']; |
99
|
|
|
$obj['version'] = $device['version']; |
100
|
|
|
$obj['serial'] = $device['serial']; |
101
|
|
|
$obj['features'] = $device['features']; |
102
|
|
|
$obj['location'] = $device['location']; |
103
|
|
|
$obj['uptime'] = $device['uptime']; |
104
|
|
|
$obj['uptime_short'] = Time::formatInterval($device['uptime'], 'short'); |
105
|
|
|
$obj['uptime_long'] = Time::formatInterval($device['uptime']); |
106
|
|
|
$obj['description'] = $device['purpose']; |
107
|
|
|
$obj['notes'] = $device['notes']; |
108
|
|
|
$obj['alert_notes'] = $alert['note']; |
109
|
|
|
$obj['device_id'] = $alert['device_id']; |
110
|
|
|
$obj['rule_id'] = $alert['rule_id']; |
111
|
|
|
$obj['id'] = $alert['id']; |
112
|
|
|
$obj['proc'] = $alert['proc']; |
113
|
|
|
$obj['status'] = $device['status']; |
114
|
|
|
$obj['status_reason'] = $device['status_reason']; |
115
|
|
|
if (can_ping_device($attribs)) { |
116
|
|
|
$ping_stats = DevicePerf::where('device_id', $alert['device_id'])->latest('timestamp')->first(); |
117
|
|
|
$obj['ping_timestamp'] = $ping_stats->timestamp; |
118
|
|
|
$obj['ping_loss'] = $ping_stats->loss; |
119
|
|
|
$obj['ping_min'] = $ping_stats->min; |
120
|
|
|
$obj['ping_max'] = $ping_stats->max; |
121
|
|
|
$obj['ping_avg'] = $ping_stats->avg; |
122
|
|
|
$obj['debug'] = json_decode($ping_stats->debug, true); |
123
|
|
|
} |
124
|
|
|
$extra = $alert['details']; |
125
|
|
|
|
126
|
|
|
$tpl = new Template; |
127
|
|
|
$template = $tpl->getTemplate($obj); |
128
|
|
|
|
129
|
|
|
if ($alert['state'] >= AlertState::ACTIVE) { |
130
|
|
|
$obj['title'] = $template->title ?: 'Alert for device ' . $device['hostname'] . ' - ' . ($alert['name'] ? $alert['name'] : $alert['rule']); |
131
|
|
|
if ($alert['state'] == AlertState::ACKNOWLEDGED) { |
132
|
|
|
$obj['title'] .= ' got acknowledged'; |
133
|
|
|
} elseif ($alert['state'] == AlertState::WORSE) { |
134
|
|
|
$obj['title'] .= ' got worse'; |
135
|
|
|
} elseif ($alert['state'] == AlertState::BETTER) { |
136
|
|
|
$obj['title'] .= ' got better'; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
foreach ($extra['rule'] as $incident) { |
140
|
|
|
$i++; |
141
|
|
|
$obj['faults'][$i] = $incident; |
142
|
|
|
$obj['faults'][$i]['string'] = null; |
143
|
|
|
foreach ($incident as $k => $v) { |
144
|
|
|
if (! empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) { |
145
|
|
|
$obj['faults'][$i]['string'] .= $k . ' = ' . $v . '; '; |
146
|
|
|
} |
147
|
|
|
} |
148
|
|
|
} |
149
|
|
|
$obj['elapsed'] = $this->timeFormat(time() - strtotime($alert['time_logged'])); |
150
|
|
|
if (! empty($extra['diff'])) { |
151
|
|
|
$obj['diff'] = $extra['diff']; |
152
|
|
|
} |
153
|
|
|
} elseif ($alert['state'] == AlertState::RECOVERED) { |
154
|
|
|
// Alert is now cleared |
155
|
|
|
$id = dbFetchRow('SELECT alert_log.id,alert_log.time_logged,alert_log.details FROM alert_log WHERE alert_log.state != ? && alert_log.state != ? && alert_log.rule_id = ? && alert_log.device_id = ? && alert_log.id < ? ORDER BY id DESC LIMIT 1', [AlertState::ACKNOWLEDGED, AlertState::RECOVERED, $alert['rule_id'], $alert['device_id'], $alert['id']]); |
156
|
|
|
if (empty($id['id'])) { |
157
|
|
|
return false; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
$extra = []; |
161
|
|
|
if (! empty($id['details'])) { |
162
|
|
|
$extra = json_decode(gzuncompress($id['details']), true); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
// Reset count to 0 so alerts will continue |
166
|
|
|
$extra['count'] = 0; |
167
|
|
|
dbUpdate(['details' => gzcompress(json_encode($id['details']), 9)], 'alert_log', 'id = ?', [$alert['id']]); |
168
|
|
|
|
169
|
|
|
$obj['title'] = $template->title_rec ?: 'Device ' . $device['hostname'] . ' recovered from ' . ($alert['name'] ? $alert['name'] : $alert['rule']); |
170
|
|
|
$obj['elapsed'] = $this->timeFormat(strtotime($alert['time_logged']) - strtotime($id['time_logged'])); |
171
|
|
|
$obj['id'] = $id['id']; |
172
|
|
|
foreach ($extra['rule'] as $incident) { |
173
|
|
|
$i++; |
174
|
|
|
$obj['faults'][$i] = $incident; |
175
|
|
|
foreach ($incident as $k => $v) { |
176
|
|
|
if (! empty($v) && $k != 'device_id' && (stristr($k, 'id') || stristr($k, 'desc') || stristr($k, 'msg')) && substr_count($k, '_') <= 1) { |
177
|
|
|
$obj['faults'][$i]['string'] .= $k . ' => ' . $v . '; '; |
178
|
|
|
} |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
} else { |
182
|
|
|
return 'Unknown State'; |
183
|
|
|
}//end if |
184
|
|
|
$obj['builder'] = $alert['builder']; |
185
|
|
|
$obj['uid'] = $alert['id']; |
186
|
|
|
$obj['alert_id'] = $alert['alert_id']; |
187
|
|
|
$obj['severity'] = $alert['severity']; |
188
|
|
|
$obj['rule'] = $alert['rule']; |
189
|
|
|
$obj['name'] = $alert['name']; |
190
|
|
|
$obj['timestamp'] = $alert['time_logged']; |
191
|
|
|
$obj['contacts'] = $extra['contacts']; |
192
|
|
|
$obj['state'] = $alert['state']; |
193
|
|
|
$obj['alerted'] = $alert['alerted']; |
194
|
|
|
$obj['template'] = $template; |
195
|
|
|
|
196
|
|
|
return $obj; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Format Elapsed Time |
201
|
|
|
* |
202
|
|
|
* @param int $secs Seconds elapsed |
203
|
|
|
* @return string |
204
|
|
|
*/ |
205
|
|
|
public function timeFormat($secs) |
206
|
|
|
{ |
207
|
|
|
$bit = [ |
208
|
|
|
'y' => $secs / 31556926 % 12, |
209
|
|
|
'w' => $secs / 604800 % 52, |
210
|
|
|
'd' => $secs / 86400 % 7, |
211
|
|
|
'h' => $secs / 3600 % 24, |
212
|
|
|
'm' => $secs / 60 % 60, |
213
|
|
|
's' => $secs % 60, |
214
|
|
|
]; |
215
|
|
|
$ret = []; |
216
|
|
|
foreach ($bit as $k => $v) { |
217
|
|
|
if ($v > 0) { |
218
|
|
|
$ret[] = $v . $k; |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
if (empty($ret)) { |
223
|
|
|
return 'none'; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
return join(' ', $ret); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
public function clearStaleAlerts() |
230
|
|
|
{ |
231
|
|
|
$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`!=' . AlertState::CLEAR . ' AND `devices`.`hostname` IS NULL'; |
232
|
|
|
foreach (dbFetchRows($sql) as $alert) { |
233
|
|
|
if (empty($alert['hostname']) && isset($alert['alert_id'])) { |
234
|
|
|
dbDelete('alerts', '`id` = ?', [$alert['alert_id']]); |
235
|
|
|
echo "Stale-alert: #{$alert['alert_id']}" . PHP_EOL; |
236
|
|
|
} |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* Re-Validate Rule-Mappings |
242
|
|
|
* |
243
|
|
|
* @param int $device_id Device-ID |
244
|
|
|
* @param int $rule Rule-ID |
245
|
|
|
* @return bool |
246
|
|
|
*/ |
247
|
|
|
public function isRuleValid($device_id, $rule) |
248
|
|
|
{ |
249
|
|
|
global $rulescache; |
250
|
|
|
if (empty($rulescache[$device_id]) || ! isset($rulescache[$device_id])) { |
251
|
|
|
foreach (AlertUtil::getRules($device_id) as $chk) { |
252
|
|
|
$rulescache[$device_id][$chk['id']] = true; |
253
|
|
|
} |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
if ($rulescache[$device_id][$rule] === true) { |
257
|
|
|
return true; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
return false; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* Issue Alert-Object |
265
|
|
|
* |
266
|
|
|
* @param array $alert |
267
|
|
|
* @return bool |
268
|
|
|
*/ |
269
|
|
|
public function issueAlert($alert) |
270
|
|
|
{ |
271
|
|
|
if (Config::get('alert.fixed-contacts') == false) { |
272
|
|
|
if (empty($alert['query'])) { |
273
|
|
|
$alert['query'] = AlertDB::genSQL($alert['rule'], $alert['builder']); |
274
|
|
|
} |
275
|
|
|
$sql = $alert['query']; |
276
|
|
|
$qry = dbFetchRows($sql, [$alert['device_id']]); |
277
|
|
|
$alert['details']['contacts'] = AlertUtil::getContacts($qry); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
$obj = $this->describeAlert($alert); |
281
|
|
|
if (is_array($obj)) { |
282
|
|
|
echo 'Issuing Alert-UID #' . $alert['id'] . '/' . $alert['state'] . ':' . PHP_EOL; |
283
|
|
|
$this->extTransports($obj); |
284
|
|
|
|
285
|
|
|
echo "\r\n"; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
return true; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Issue ACK notification |
293
|
|
|
* |
294
|
|
|
* @return void |
295
|
|
|
*/ |
296
|
|
|
public function runAcks() |
297
|
|
|
{ |
298
|
|
|
foreach ($this->loadAlerts('alerts.state = ' . AlertState::ACKNOWLEDGED . ' && alerts.open = ' . AlertState::ACTIVE) as $alert) { |
299
|
|
|
$this->issueAlert($alert); |
300
|
|
|
dbUpdate(['open' => AlertState::CLEAR], 'alerts', 'rule_id = ? && device_id = ?', [$alert['rule_id'], $alert['device_id']]); |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* Run Follow-Up alerts |
306
|
|
|
* |
307
|
|
|
* @return void |
308
|
|
|
*/ |
309
|
|
|
public function runFollowUp() |
310
|
|
|
{ |
311
|
|
|
foreach ($this->loadAlerts('alerts.state > ' . AlertState::CLEAR . ' && alerts.open = 0') as $alert) { |
312
|
|
|
if ($alert['state'] != AlertState::ACKNOWLEDGED || ($alert['info']['until_clear'] === false)) { |
313
|
|
|
$rextra = json_decode($alert['extra'], true); |
314
|
|
|
if ($rextra['invert']) { |
315
|
|
|
continue; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
if (empty($alert['query'])) { |
319
|
|
|
$alert['query'] = AlertDB::genSQL($alert['rule'], $alert['builder']); |
320
|
|
|
} |
321
|
|
|
$chk = dbFetchRows($alert['query'], [$alert['device_id']]); |
322
|
|
|
//make sure we can json_encode all the datas later |
323
|
|
|
$cnt = count($chk); |
324
|
|
|
for ($i = 0; $i < $cnt; $i++) { |
325
|
|
|
if (isset($chk[$i]['ip'])) { |
326
|
|
|
$chk[$i]['ip'] = inet6_ntop($chk[$i]['ip']); |
327
|
|
|
} |
328
|
|
|
} |
329
|
|
|
$o = sizeof($alert['details']['rule']); |
330
|
|
|
$n = sizeof($chk); |
331
|
|
|
$ret = 'Alert #' . $alert['id']; |
332
|
|
|
$state = AlertState::CLEAR; |
333
|
|
|
if ($n > $o) { |
334
|
|
|
$ret .= ' Worsens'; |
335
|
|
|
$state = AlertState::WORSE; |
336
|
|
|
$alert['details']['diff'] = array_diff($chk, $alert['details']['rule']); |
337
|
|
|
} elseif ($n < $o) { |
338
|
|
|
$ret .= ' Betters'; |
339
|
|
|
$state = AlertState::BETTER; |
340
|
|
|
$alert['details']['diff'] = array_diff($alert['details']['rule'], $chk); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
if ($state > AlertState::CLEAR && $n > 0) { |
344
|
|
|
$alert['details']['rule'] = $chk; |
345
|
|
|
if (dbInsert([ |
346
|
|
|
'state' => $state, |
347
|
|
|
'device_id' => $alert['device_id'], |
348
|
|
|
'rule_id' => $alert['rule_id'], |
349
|
|
|
'details' => gzcompress(json_encode($alert['details']), 9), |
350
|
|
|
], 'alert_log')) { |
351
|
|
|
dbUpdate(['state' => $state, 'open' => 1, 'alerted' => 1], 'alerts', 'rule_id = ? && device_id = ?', [$alert['rule_id'], $alert['device_id']]); |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
echo $ret . ' (' . $o . '/' . $n . ")\r\n"; |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
public function loadAlerts($where) |
361
|
|
|
{ |
362
|
|
|
$alerts = []; |
363
|
|
|
foreach (dbFetchRows("SELECT alerts.id, alerts.alerted, alerts.device_id, alerts.rule_id, alerts.state, alerts.note, alerts.info FROM alerts WHERE $where") as $alert_status) { |
364
|
|
|
$alert = dbFetchRow( |
365
|
|
|
'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,alert_rules.proc 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', |
366
|
|
|
[$alert_status['device_id'], $alert_status['rule_id']] |
367
|
|
|
); |
368
|
|
|
|
369
|
|
|
if (empty($alert['rule_id']) || ! $this->isRuleValid($alert_status['device_id'], $alert_status['rule_id'])) { |
370
|
|
|
echo 'Stale-Rule: #' . $alert_status['rule_id'] . '/' . $alert_status['device_id'] . "\r\n"; |
371
|
|
|
// Alert-Rule does not exist anymore, let's remove the alert-state. |
372
|
|
|
dbDelete('alerts', 'rule_id = ? && device_id = ?', [$alert_status['rule_id'], $alert_status['device_id']]); |
373
|
|
|
} else { |
374
|
|
|
$alert['alert_id'] = $alert_status['id']; |
375
|
|
|
$alert['state'] = $alert_status['state']; |
376
|
|
|
$alert['alerted'] = $alert_status['alerted']; |
377
|
|
|
$alert['note'] = $alert_status['note']; |
378
|
|
|
if (! empty($alert['details'])) { |
379
|
|
|
$alert['details'] = json_decode(gzuncompress($alert['details']), true); |
380
|
|
|
} |
381
|
|
|
$alert['info'] = json_decode($alert_status['info'], true); |
382
|
|
|
$alerts[] = $alert; |
383
|
|
|
} |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
return $alerts; |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* Run all alerts |
391
|
|
|
* |
392
|
|
|
* @return void |
393
|
|
|
*/ |
394
|
|
|
public function runAlerts() |
395
|
|
|
{ |
396
|
|
|
foreach ($this->loadAlerts('alerts.state != ' . AlertState::ACKNOWLEDGED . ' && alerts.open = 1') as $alert) { |
397
|
|
|
$noiss = false; |
398
|
|
|
$noacc = false; |
399
|
|
|
$updet = false; |
400
|
|
|
$rextra = json_decode($alert['extra'], true); |
401
|
|
|
if (! isset($rextra['recovery'])) { |
402
|
|
|
// backwards compatibility check |
403
|
|
|
$rextra['recovery'] = true; |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
$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 = ?', [$alert['device_id'], $alert['rule_id']]); |
407
|
|
|
|
408
|
|
|
if ($chk['alerted'] == $alert['state']) { |
409
|
|
|
$noiss = true; |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
$tolerence_window = Config::get('alert.tolerance_window'); |
413
|
|
|
if (! empty($rextra['count']) && empty($rextra['interval'])) { |
414
|
|
|
// This check below is for compat-reasons |
415
|
|
|
if (! empty($rextra['delay']) && $alert['state'] != AlertState::RECOVERED) { |
416
|
|
|
if ((time() - strtotime($alert['time_logged']) + $tolerence_window) < $rextra['delay'] || (! empty($alert['details']['delay']) && (time() - $alert['details']['delay'] + $tolerence_window) < $rextra['delay'])) { |
417
|
|
|
continue; |
418
|
|
|
} else { |
419
|
|
|
$alert['details']['delay'] = time(); |
420
|
|
|
$updet = true; |
421
|
|
|
} |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
if ($alert['state'] == AlertState::ACTIVE && ! empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) { |
425
|
|
|
if ($alert['details']['count'] < $rextra['count']) { |
426
|
|
|
$noacc = true; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
$updet = true; |
430
|
|
|
$noiss = false; |
431
|
|
|
} |
432
|
|
|
} else { |
433
|
|
|
// This is the new way |
434
|
|
|
if (! empty($rextra['delay']) && (time() - strtotime($alert['time_logged']) + $tolerence_window) < $rextra['delay'] && $alert['state'] != AlertState::RECOVERED) { |
435
|
|
|
continue; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
if (! empty($rextra['interval'])) { |
439
|
|
|
if (! empty($alert['details']['interval']) && (time() - $alert['details']['interval'] + $tolerence_window) < $rextra['interval']) { |
440
|
|
|
continue; |
441
|
|
|
} else { |
442
|
|
|
$alert['details']['interval'] = time(); |
443
|
|
|
$updet = true; |
444
|
|
|
} |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
if (in_array($alert['state'], [AlertState::ACTIVE, AlertState::WORSE, AlertState::BETTER]) && ! empty($rextra['count']) && ($rextra['count'] == -1 || $alert['details']['count']++ < $rextra['count'])) { |
448
|
|
|
if ($alert['details']['count'] < $rextra['count']) { |
449
|
|
|
$noacc = true; |
450
|
|
|
} |
451
|
|
|
|
452
|
|
|
$updet = true; |
453
|
|
|
$noiss = false; |
454
|
|
|
} |
455
|
|
|
} |
456
|
|
|
if ($chk['ignore'] == 1 || $chk['disabled'] == 1) { |
457
|
|
|
$noiss = true; |
458
|
|
|
$updet = false; |
459
|
|
|
$noacc = false; |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
if (AlertUtil::isMaintenance($alert['device_id'])) { |
463
|
|
|
$noiss = true; |
464
|
|
|
$noacc = true; |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
if ($updet) { |
468
|
|
|
dbUpdate(['details' => gzcompress(json_encode($alert['details']), 9)], 'alert_log', 'id = ?', [$alert['id']]); |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
if (! empty($rextra['mute'])) { |
472
|
|
|
echo 'Muted Alert-UID #' . $alert['id'] . "\r\n"; |
473
|
|
|
$noiss = true; |
474
|
|
|
} |
475
|
|
|
|
476
|
|
|
if ($this->isParentDown($alert['device_id'])) { |
477
|
|
|
$noiss = true; |
478
|
|
|
Log::event('Skipped alerts because all parent devices are down', $alert['device_id'], 'alert', 1); |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
if ($alert['state'] == AlertState::RECOVERED && $rextra['recovery'] == false) { |
482
|
|
|
// Rule is set to not send a recovery alert |
483
|
|
|
$noiss = true; |
484
|
|
|
} |
485
|
|
|
|
486
|
|
|
if (! $noiss) { |
487
|
|
|
$this->issueAlert($alert); |
488
|
|
|
dbUpdate(['alerted' => $alert['state']], 'alerts', 'rule_id = ? && device_id = ?', [$alert['rule_id'], $alert['device_id']]); |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
if (! $noacc) { |
492
|
|
|
dbUpdate(['open' => 0], 'alerts', 'rule_id = ? && device_id = ?', [$alert['rule_id'], $alert['device_id']]); |
493
|
|
|
} |
494
|
|
|
} |
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
/** |
498
|
|
|
* Run external transports |
499
|
|
|
* |
500
|
|
|
* @param array $obj Alert-Array |
501
|
|
|
* @return void |
502
|
|
|
*/ |
503
|
|
|
public function extTransports($obj) |
504
|
|
|
{ |
505
|
|
|
$type = new Template; |
506
|
|
|
|
507
|
|
|
// If alert transport mapping exists, override the default transports |
508
|
|
|
$transport_maps = AlertUtil::getAlertTransports($obj['alert_id']); |
509
|
|
|
|
510
|
|
|
if (! $transport_maps) { |
511
|
|
|
$transport_maps = AlertUtil::getDefaultAlertTransports(); |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
// alerting for default contacts, etc |
515
|
|
|
if (Config::get('alert.transports.mail') === true && ! empty($obj['contacts'])) { |
516
|
|
|
$transport_maps[] = [ |
517
|
|
|
'transport_id' => null, |
518
|
|
|
'transport_type' => 'mail', |
519
|
|
|
'opts' => $obj, |
520
|
|
|
]; |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
foreach ($transport_maps as $item) { |
524
|
|
|
$class = 'LibreNMS\\Alert\\Transport\\' . ucfirst($item['transport_type']); |
525
|
|
|
if (class_exists($class)) { |
526
|
|
|
//FIXME remove Deprecated transport |
527
|
|
|
$transport_title = "Transport {$item['transport_type']}"; |
528
|
|
|
$obj['transport'] = $item['transport_type']; |
529
|
|
|
$obj['transport_name'] = $item['transport_name']; |
530
|
|
|
$obj['alert'] = new AlertData($obj); |
531
|
|
|
$obj['title'] = $type->getTitle($obj); |
532
|
|
|
$obj['alert']['title'] = $obj['title']; |
533
|
|
|
$obj['msg'] = $type->getBody($obj); |
534
|
|
|
c_echo(" :: $transport_title => "); |
535
|
|
|
try { |
536
|
|
|
$instance = new $class($item['transport_id']); |
537
|
|
|
$tmp = $instance->deliverAlert($obj, $item['opts']); |
538
|
|
|
$this->alertLog($tmp, $obj, $obj['transport']); |
539
|
|
|
} catch (\Exception $e) { |
540
|
|
|
$this->alertLog($e, $obj, $obj['transport']); |
541
|
|
|
} |
542
|
|
|
unset($instance); |
543
|
|
|
echo PHP_EOL; |
544
|
|
|
} |
545
|
|
|
} |
546
|
|
|
|
547
|
|
|
if (count($transport_maps) === 0) { |
548
|
|
|
echo 'No configured transports'; |
549
|
|
|
} |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
// Log alert event |
553
|
|
|
public function alertLog($result, $obj, $transport) |
554
|
|
|
{ |
555
|
|
|
$prefix = [ |
556
|
|
|
AlertState::RECOVERED => 'recovery', |
557
|
|
|
AlertState::ACTIVE => $obj['severity'] . ' alert', |
558
|
|
|
AlertState::ACKNOWLEDGED => 'acknowledgment', |
559
|
|
|
]; |
560
|
|
|
$prefix[3] = &$prefix[0]; |
561
|
|
|
$prefix[4] = &$prefix[0]; |
562
|
|
|
|
563
|
|
|
if ($obj['state'] == AlertState::RECOVERED) { |
564
|
|
|
$severity = Alert::OK; |
565
|
|
|
} elseif ($obj['state'] == AlertState::ACTIVE) { |
566
|
|
|
$severity = Alert::SEVERITIES[$obj['severity']] ?? Alert::UNKNOWN; |
567
|
|
|
} elseif ($obj['state'] == AlertState::ACKNOWLEDGED) { |
568
|
|
|
$severity = Alert::NOTICE; |
569
|
|
|
} else { |
570
|
|
|
$severity = Alert::UNKNOWN; |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
if ($result === true) { |
574
|
|
|
echo 'OK'; |
575
|
|
|
Log::event('Issued ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], 'alert', $severity); |
576
|
|
|
} elseif ($result === false) { |
577
|
|
|
echo 'ERROR'; |
578
|
|
|
Log::event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "'", $obj['device_id'], null, Alert::ERROR); |
579
|
|
|
} else { |
580
|
|
|
echo "ERROR: $result\r\n"; |
581
|
|
|
Log::event('Could not issue ' . $prefix[$obj['state']] . " for rule '" . $obj['name'] . "' to transport '" . $transport . "' Error: " . $result, $obj['device_id'], 'error', Alert::ERROR); |
582
|
|
|
} |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
/** |
586
|
|
|
* Check if a device's all parent are down |
587
|
|
|
* Returns true if all parents are down |
588
|
|
|
* |
589
|
|
|
* @param int $device Device-ID |
590
|
|
|
* @return bool |
591
|
|
|
*/ |
592
|
|
|
public function isParentDown($device) |
593
|
|
|
{ |
594
|
|
|
$parent_count = dbFetchCell('SELECT count(*) from `device_relationships` WHERE `child_device_id` = ?', [$device]); |
595
|
|
|
if (! $parent_count) { |
596
|
|
|
return false; |
597
|
|
|
} |
598
|
|
|
|
599
|
|
|
$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))", [$device]); |
600
|
|
|
if ($down_parent_count == $parent_count) { |
601
|
|
|
return true; |
602
|
|
|
} |
603
|
|
|
|
604
|
|
|
return false; |
605
|
|
|
} |
606
|
|
|
} |
607
|
|
|
|