1 | <?php |
||
2 | /* |
||
3 | * LibreNMS |
||
4 | * |
||
5 | * Copyright (c) 2018 Neil Lathwood <https://github.com/laf/ http://www.lathwood.co.uk/fa> |
||
6 | * Copyright (c) 2018 Tony Murray <[email protected]> |
||
7 | * |
||
8 | * This program is free software: you can redistribute it and/or modify it |
||
9 | * under the terms of the GNU General Public License as published by the |
||
10 | * Free Software Foundation, either version 3 of the License, or (at your |
||
11 | * option) any later version. Please see LICENSE.txt at the top level of |
||
12 | * the source code distribution for details. |
||
13 | */ |
||
14 | |||
15 | use LibreNMS\Alerting\QueryBuilderFilter; |
||
16 | use LibreNMS\Config; |
||
0 ignored issues
–
show
|
|||
17 | |||
18 | $default_severity = Config::get('alert_rule.severity'); |
||
19 | $default_max_alerts = Config::get('alert_rule.max_alerts'); |
||
20 | $default_delay = Config::get('alert_rule.delay') . 'm'; |
||
21 | $default_interval = Config::get('alert_rule.interval') . 'm'; |
||
22 | $default_mute_alerts = Config::get('alert_rule.mute_alerts'); |
||
23 | $default_invert_rule_match = Config::get('alert_rule.invert_rule_match'); |
||
24 | $default_recovery_alerts = Config::get('alert_rule.recovery_alerts'); |
||
25 | $default_invert_map = Config::get('alert_rule.invert_map'); |
||
26 | |||
27 | if (Auth::user()->hasGlobalAdmin()) { |
||
28 | $filters = json_encode(new QueryBuilderFilter('alert')); ?> |
||
29 | |||
30 | <div class="modal fade" id="create-alert" tabindex="-1" role="dialog" |
||
31 | aria-labelledby="Create" aria-hidden="true"> |
||
32 | <div class="modal-dialog modal-lg"> |
||
33 | <div class="modal-content"> |
||
34 | <div class="modal-header"> |
||
35 | <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> |
||
36 | <h5 class="modal-title" id="Create">Alert Rule :: <a target="_blank" href="https://docs.librenms.org/Alerting/"><i class="fa fa-book fa-1x"></i> Docs</a> </h5> |
||
37 | </div> |
||
38 | <div class="modal-body"> |
||
39 | <ul class="nav nav-tabs" role="tablist"> |
||
40 | <li role="presentation" class="active"><a href="#main" aria-controls="main" role="tab" data-toggle="tab">Main </a></li> |
||
41 | <li role="presentation"><a href="#advanced" aria-controls="advanced" role="tab" data-toggle="tab">Advanced</a></li> |
||
42 | </ul> |
||
43 | <br /> |
||
44 | <form method="post" role="form" id="rules" class="form-horizontal alerts-form"> |
||
45 | <?php echo csrf_field() ?> |
||
46 | <input type="hidden" name="device_id" id="device_id" value="<?php echo isset($device['device_id']) ? $device['device_id'] : -1; ?>"> |
||
47 | <input type="hidden" name="device_name" id="device_name" value="<?php echo format_hostname($device); ?>"> |
||
48 | <input type="hidden" name="rule_id" id="rule_id" value=""> |
||
49 | <input type="hidden" name="type" id="type" value="alert-rules"> |
||
50 | <input type="hidden" name="template_id" id="template_id" value=""> |
||
51 | <input type="hidden" name="builder_json" id="builder_json" value=""> |
||
52 | <div class="tab-content"> |
||
53 | <div role="tabpanel" class="tab-pane active" id="main"> |
||
54 | <div class='form-group' title="The description of this alert rule."> |
||
55 | <label for='rule_name' class='col-sm-3 col-md-2 control-label'>Rule name: </label> |
||
56 | <div class='col-sm-9 col-md-10'> |
||
57 | <input type='text' id='rule_name' name='name' class='form-control validation' maxlength='200' required> |
||
58 | </div> |
||
59 | </div> |
||
60 | <div class="form-group"> |
||
61 | <div class="col-sm-3 col-md-2"> |
||
62 | <div class="dropdown"> |
||
63 | <button class="btn btn-default dropdown-toggle" type="button" |
||
64 | id="import-from" data-toggle="dropdown" aria-haspopup="true" |
||
65 | aria-expanded="true"> |
||
66 | Import from |
||
67 | <span class="caret"></span> |
||
68 | </button> |
||
69 | <ul class="dropdown-menu" aria-labelledby="import-from" id="import-dropdown"> |
||
70 | <li><a href="#" name="import-query" id="import-query">SQL Query</a></li> |
||
71 | <li><a href="#" name="import-old-format" id="import-old-format">Old Format</a></li> |
||
72 | <li><a href="#" name="import-collection" id="import-collection">Collection</a></li> |
||
73 | <li><a href="#" name="import-alert_rule" id="import-alert_rule">Alert Rule</a></li> |
||
74 | </ul> |
||
75 | </div> |
||
76 | </div> |
||
77 | <div class="col-sm-9 col-md-10"> |
||
78 | <div id="builder"></div> |
||
79 | </div> |
||
80 | </div> |
||
81 | <div class="form-group" title="How to display the alert. OK: green, Warning: yellow, Critical: red"> |
||
82 | <label for='severity' class='col-sm-3 col-md-2 control-label'>Severity: </label> |
||
83 | <div class="col-sm-2"> |
||
84 | <select name='severity' id='severity' class='form-control'> |
||
85 | <option value='ok'>OK</option> |
||
86 | <option value='warning'>Warning</option> |
||
87 | <option value='critical' selected>Critical</option> |
||
88 | </select> |
||
89 | </div> |
||
90 | </div> |
||
91 | <div class="form-group form-inline"> |
||
92 | <label for='count' class='col-sm-3 col-md-2 control-label' title="How many notifications to issue while active before stopping. -1 means no limit. If interval is 0, this has no effect.">Max alerts: </label> |
||
93 | <div class="col-sm-2" title="How many notifications to issue while active before stopping. -1 means no limit. If interval is 0, this has no effect."> |
||
94 | <input type='text' id='count' name='count' class='form-control' size="4" value="123"> |
||
95 | </div> |
||
96 | <div class="col-sm-3" title="How long to wait before issuing a notification. If the alert clears before the delay, no notification will be issued. (s,m,h,d)"> |
||
97 | <label for='delay' class='control-label' style="vertical-align: top;">Delay: </label> |
||
98 | <input type='text' id='delay' name='delay' class='form-control' size="4"> |
||
99 | </div> |
||
100 | <div class="col-sm-4 col-md-3" title="How often to re-issue notifications while this alert is active. 0 means notify once. This is affected by the poller interval. (s,m,h,d)"> |
||
101 | <label for='interval' class='control-label' style="vertical-align: top;">Interval: </label> |
||
102 | <input type='text' id='interval' name='interval' class='form-control' size="4"> |
||
103 | </div> |
||
104 | </div> |
||
105 | <div class='form-group form-inline'> |
||
106 | <label for='mute' class='col-sm-3 col-md-2 control-label' title="Show alert status in the webui, but do not issue notifications.">Mute alerts: </label> |
||
107 | <div class='col-sm-2' title="Show alert status in the webui, but do not issue notifications."> |
||
108 | <input type="checkbox" name="mute" id="mute"> |
||
109 | </div> |
||
110 | <label for='invert' class='col-sm-3 col-md-3 control-label' title="Alert when this rule doesn't match." style="vertical-align: top;">Invert rule match: </label> |
||
111 | <div class='col-sm-2' title="Alert when this rule doesn't match."> |
||
112 | <input type='checkbox' name='invert' id='invert'> |
||
113 | </div> |
||
114 | </div> |
||
115 | <div class="form-group" title="Issue recovery notifications."> |
||
116 | <label for='recovery' class='col-sm-3 col-md-2 control-label'>Recovery alerts: </label> |
||
117 | <div class='col-sm-2'> |
||
118 | <input type='checkbox' name='recovery' id='recovery'> |
||
119 | </div> |
||
120 | </div> |
||
121 | <div class="form-group form-inline"> |
||
122 | <label for='maps' class='col-sm-3 col-md-2 control-label' title="Restricts this alert rule to the selected devices, groups and locations.">Match devices, groups and locations list: </label> |
||
123 | <div class="col-sm-7" style="width: 56%;"> |
||
124 | <select id="maps" name="maps[]" class="form-control" multiple="multiple"></select> |
||
125 | </div> |
||
126 | <div> |
||
127 | <label for='invert_map' class='col-md-1' style="width: 14.1333%;" text-align="left" title="If ON, alert rule check will run on all devices except the selected devices and groups.">All devices except in list: </label> |
||
128 | <input type='checkbox' name='invert_map' id='invert_map'> |
||
129 | </div> |
||
130 | </div> |
||
131 | <div class="form-group" title="Restricts this alert rule to specified transports."> |
||
132 | <label for="transports" class="col-sm-3 col-md-2 control-label">Transports: </label> |
||
133 | <div class="col-sm-9 col-md-10"> |
||
134 | <select id="transports" name="transports[]" class="form-control" multiple="multiple"></select> |
||
135 | </div> |
||
136 | </div> |
||
137 | <div class='form-group' title="A link to some documentation on how to handle this alert. This will be included in notifications."> |
||
138 | <label for='proc' class='col-sm-3 col-md-2 control-label'>Procedure URL: </label> |
||
139 | <div class='col-sm-9 col-md-10'> |
||
140 | <input type='text' id='proc' name='proc' class='form-control validation' pattern='(http|https)://.*' maxlength='80'> |
||
141 | </div> |
||
142 | </div> |
||
143 | </div> |
||
144 | <div role="tabpanel" class="tab-pane" id="advanced"> |
||
145 | <div class="form-group"> |
||
146 | <label for="override_query" class="col-sm-3 col-md-2 control-label">Override SQL</label> |
||
147 | <div class="col-sm-9 col-md-10"> |
||
148 | <input type='checkbox' name='override_query' id='override_query'> |
||
149 | </div> |
||
150 | </div> |
||
151 | <div class="form-group"> |
||
152 | <label for="adv_query" class="col-sm-3 col-md-2 control-label">Query</label> |
||
153 | <div class="col-sm-9 col-md-10"> |
||
154 | <input type="text" id="adv_query" name="adv_query" class="form-control"> |
||
155 | </div> |
||
156 | </div> |
||
157 | </div> |
||
158 | </div> |
||
159 | <div class="form-group"> |
||
160 | <div class="col-sm-12 text-center"> |
||
161 | <button type="button" class="btn btn-success" id="btn-save" name="save-alert"> |
||
162 | Save Rule |
||
163 | </button> |
||
164 | </div> |
||
165 | </div> |
||
166 | </form> |
||
167 | </div> |
||
168 | </div> |
||
169 | </div> |
||
170 | </div> |
||
171 | |||
172 | <script src="js/sql-parser.min.js"></script> |
||
173 | <script src="js/query-builder.standalone.min.js"></script> |
||
174 | <script> |
||
175 | $('#builder').on('afterApplyRuleFlags.queryBuilder afterCreateRuleFilters.queryBuilder', function () { |
||
176 | $("[name$='_filter']").each(function () { |
||
177 | $(this).select2({ |
||
178 | dropdownParent: $("#create-alert"), |
||
179 | dropdownAutoWidth : true, |
||
180 | width: 'auto' |
||
181 | }); |
||
182 | }); |
||
183 | }).on('ruleToSQL.queryBuilder.filter', function (e, rule) { |
||
184 | if (rule.operator === 'regexp') { |
||
185 | e.value += ' \'' + rule.value + '\''; |
||
186 | } |
||
187 | }).queryBuilder({ |
||
188 | plugins: [ |
||
189 | 'bt-tooltip-errors' |
||
190 | // 'not-group' |
||
191 | ], |
||
192 | |||
193 | filters: <?php echo $filters; ?>, |
||
194 | operators: [ |
||
195 | 'equal', 'not_equal', 'between', 'not_between', 'begins_with', 'not_begins_with', 'contains', 'not_contains', 'ends_with', 'not_ends_with', 'is_empty', 'is_not_empty', 'is_null', 'is_not_null', |
||
196 | {type: 'less', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']}, |
||
197 | {type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']}, |
||
198 | {type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']}, |
||
199 | {type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']}, |
||
200 | {type: 'regex', nb_inputs: 1, multiple: false, apply_to: ['string', 'number']}, |
||
201 | {type: 'not_regex', nb_inputs: 1, multiple: false, apply_to: ['string', 'number']} |
||
202 | ], |
||
203 | lang: { |
||
204 | operators: { |
||
205 | regexp: 'regex', |
||
206 | not_regex: 'not regex' |
||
207 | } |
||
208 | }, |
||
209 | sqlOperators: { |
||
210 | regexp: {op: 'REGEXP'}, |
||
211 | not_regexp: {op: 'NOT REGEXP'} |
||
212 | }, |
||
213 | sqlRuleOperator: { |
||
214 | 'REGEXP': function (v) { |
||
215 | return {val: v, op: 'regexp'}; |
||
216 | }, |
||
217 | 'NOT REGEXP': function (v) { |
||
218 | return {val: v, op: 'not_regexp'}; |
||
219 | } |
||
220 | } |
||
221 | }); |
||
222 | |||
223 | $('#btn-save').on('click', function (e) { |
||
224 | e.preventDefault(); |
||
225 | var result_json = $('#builder').queryBuilder('getRules'); |
||
226 | if (result_json !== null && result_json.valid) { |
||
227 | $('#builder_json').val(JSON.stringify(result_json)); |
||
228 | $.ajax({ |
||
229 | type: "POST", |
||
230 | url: "ajax_form.php", |
||
231 | data: $('form.alerts-form').serializeArray(), |
||
232 | dataType: "json", |
||
233 | success: function (data) { |
||
234 | if (data.status == 'ok') { |
||
235 | toastr.success(data.message); |
||
236 | $('#create-alert').modal('hide'); |
||
237 | window.location.reload(); // FIXME: reload table |
||
238 | } else { |
||
239 | toastr.error(data.message); |
||
240 | } |
||
241 | }, |
||
242 | error: function () { |
||
243 | toastr.error('Failed to process rule'); |
||
244 | } |
||
245 | }); |
||
246 | } |
||
247 | }); |
||
248 | $('#import-query').on('click', function (e) { |
||
249 | e.preventDefault(); |
||
250 | var sql_import = window.prompt("Enter your SQL query:"); |
||
251 | if (sql_import) { |
||
252 | try { |
||
253 | $("#builder").queryBuilder("setRulesFromSQL", sql_import); |
||
254 | } catch (e) { |
||
255 | alert('Your query could not be parsed'); |
||
256 | } |
||
257 | } |
||
258 | }); |
||
259 | |||
260 | $('#import-old-format').on('click', function (e) { |
||
261 | e.preventDefault(); |
||
262 | var old_import = window.prompt("Enter your old alert rule:"); |
||
263 | if (old_import) { |
||
264 | try { |
||
265 | old_import = old_import.replace(/&&/g, 'AND'); |
||
266 | old_import = old_import.replace(/\|\|/g, 'OR'); |
||
267 | old_import = old_import.replace(/%/g, ''); |
||
268 | old_import = old_import.replace(/"/g, "'"); |
||
269 | old_import = old_import.replace(/~/g, "REGEXP"); |
||
270 | old_import = old_import.replace(/@/g, ".*"); |
||
271 | $("#builder").queryBuilder("setRulesFromSQL", old_import); |
||
272 | } catch (e) { |
||
273 | alert('Your query could not be parsed'); |
||
274 | } |
||
275 | } |
||
276 | }); |
||
277 | |||
278 | $('#import-collection').on('click', function (e) { |
||
279 | e.preventDefault(); |
||
280 | $("#search_rule_modal").modal('show'); |
||
281 | }); |
||
282 | |||
283 | $('#import-alert_rule').on('click', function (e) { |
||
284 | e.preventDefault(); |
||
285 | $("#search_alert_rule_modal").modal('show'); |
||
286 | }); |
||
287 | |||
288 | $('#create-alert').on('show.bs.modal', function(e) { |
||
289 | //get data-id attribute of the clicked element |
||
290 | var rule_id = $(e.relatedTarget).data('rule_id'); |
||
291 | $('#rule_id').val(rule_id); |
||
292 | |||
293 | if (rule_id >= 0) { |
||
294 | $.ajax({ |
||
295 | type: "POST", |
||
296 | url: "ajax_form.php", |
||
297 | data: { type: "parse-alert-rule", alert_id: rule_id }, |
||
298 | dataType: "json", |
||
299 | success: function (data) { |
||
300 | loadRule(data); |
||
301 | } |
||
302 | }); |
||
303 | } else { |
||
304 | // new, reset form |
||
305 | $("#builder").queryBuilder("reset"); |
||
306 | var $severity = $('#severity'); |
||
307 | $severity.val($severity.find("option[selected]").val()); |
||
308 | $("#mute").bootstrapSwitch('state', <?=$default_mute_alerts?>); |
||
309 | $("#invert").bootstrapSwitch('state', <?=$default_invert_rule_match?>); |
||
310 | $("#recovery").bootstrapSwitch('state', <?=$default_recovery_alerts?>); |
||
311 | $("#override_query").bootstrapSwitch('state', false); |
||
312 | $("#invert_map").bootstrapSwitch('state', <?=$default_invert_map?>); |
||
313 | $(this).find("input[type=text]").val(""); |
||
314 | $('#count').val('<?=$default_max_alerts?>'); |
||
315 | $('#delay').val('<?=$default_delay?>'); |
||
316 | $('#interval').val('<?=$default_interval?>'); |
||
317 | $('#adv_query').val(''); |
||
318 | $('#severity').val('<?=$default_severity?>'); |
||
319 | |||
320 | var $maps = $('#maps'); |
||
321 | $maps.empty(); |
||
322 | $maps.val(null).trigger('change'); |
||
323 | setRuleDevice();// pre-populate device in the maps if this is a per-device rule |
||
324 | |||
325 | var $transports = $("#transports"); |
||
326 | $transports.empty(); |
||
327 | $transports.val(null).trigger('change'); |
||
328 | $("#transport-choice").val("email"); |
||
329 | } |
||
330 | }); |
||
331 | |||
332 | function loadRule(rule) { |
||
333 | $('#rule_name').val(rule.name); |
||
334 | $('#proc').val(rule.proc); |
||
335 | $('#builder').queryBuilder("setRules", rule.builder); |
||
336 | $('#severity').val(rule.severity).trigger('change'); |
||
337 | $('#adv_query').val(rule.adv_query); |
||
338 | |||
339 | var $maps = $('#maps'); |
||
340 | $maps.empty(); |
||
341 | $maps.val(null).trigger('change'); // clear |
||
342 | if (rule.maps == null) { |
||
343 | // collection rule |
||
344 | setRuleDevice() |
||
345 | } else { |
||
346 | $.each(rule.maps, function(index, value) { |
||
347 | var option = new Option(value.text, value.id, true, true); |
||
348 | $maps.append(option).trigger('change') |
||
349 | }); |
||
350 | } |
||
351 | var $transports = $("#transports"); |
||
352 | $transports.empty(); |
||
353 | $transports.val(null).trigger('change'); |
||
354 | if(rule.transports != null) { |
||
355 | $.each(rule.transports, function(index, value) { |
||
356 | var option = new Option(value.text, value.id, true, true); |
||
357 | $transports.append(option).trigger("change"); |
||
358 | }); |
||
359 | } |
||
360 | |||
361 | if (rule.extra != null) { |
||
362 | var extra = rule.extra; |
||
363 | $('#count').val(extra.count); |
||
364 | if ((extra.delay / 86400) >= 1) { |
||
365 | $('#delay').val(extra.delay / 86400 + 'd'); |
||
366 | } else if ((extra.delay / 3600) >= 1) { |
||
367 | $('#delay').val( extra.delay / 3600 + 'h'); |
||
368 | } else if ((extra.delay / 60) >= 1) { |
||
369 | $('#delay').val( extra.delay / 60 + 'm'); |
||
370 | } else { |
||
371 | $('#delay').val(extra.delay); |
||
372 | } |
||
373 | |||
374 | if ((extra.interval / 86400) >= 1) { |
||
375 | $('#interval').val(extra.interval / 86400 + 'd'); |
||
376 | } else if ((extra.interval / 3600) >= 1) { |
||
377 | $('#interval').val(extra.interval / 3600 + 'h'); |
||
378 | } else if ((extra.interval / 60) >= 1) { |
||
379 | $('#interval').val(extra.interval / 60 + 'm'); |
||
380 | } else { |
||
381 | $('#interval').val(extra.interval); |
||
382 | } |
||
383 | |||
384 | if (extra.adv_query) { |
||
385 | $('#adv_query').val(extra.adv_query); |
||
386 | } |
||
387 | $("[name='mute']").bootstrapSwitch('state', extra.mute); |
||
388 | $("[name='invert']").bootstrapSwitch('state', extra.invert); |
||
389 | if (typeof extra.recovery == 'undefined') { |
||
390 | extra.recovery = '<?=$default_recovery_alerts?>'; |
||
391 | } |
||
392 | |||
393 | if (typeof extra.options == 'undefined') { |
||
394 | extra.options = new Array(); |
||
395 | } |
||
396 | if (typeof extra.options.override_query == 'undefined') { |
||
397 | extra.options.override_query = false; |
||
398 | } |
||
399 | $("[name='recovery']").bootstrapSwitch('state', extra.recovery); |
||
400 | |||
401 | if (rule.invert_map == 1) { |
||
402 | $("[name='invert_map']").bootstrapSwitch('state', true); |
||
403 | }else{ |
||
404 | $("[name='invert_map']").bootstrapSwitch('state', false); |
||
405 | } |
||
406 | |||
407 | $("[name='override_query']").bootstrapSwitch('state', extra.options.override_query); |
||
408 | } else { |
||
409 | $('#count').val('<?=$default_max_alerts?>'); |
||
410 | } |
||
411 | } |
||
412 | |||
413 | function setRuleDevice() { |
||
414 | // pre-populate device in the maps if this is a per-device rule |
||
415 | var device_id = $('#device_id').val(); |
||
416 | if (device_id > 0) { |
||
417 | var device_name = $('#device_name').val(); |
||
418 | var option = new Option(device_name, device_id, true, true); |
||
419 | $('#maps').append(option).trigger('change') |
||
420 | } |
||
421 | } |
||
422 | |||
423 | $("#maps").select2({ |
||
424 | width: '100%', |
||
425 | placeholder: "Devices, Groups or Locations", |
||
426 | ajax: { |
||
427 | url: 'ajax_list.php', |
||
428 | delay: 250, |
||
429 | data: function (params) { |
||
430 | return { |
||
431 | type: 'devices_groups_locations', |
||
432 | search: params.term |
||
433 | }; |
||
434 | } |
||
435 | } |
||
436 | }); |
||
437 | |||
438 | $("#transports").select2({ |
||
439 | width: "100%", |
||
440 | placeholder: "Transport/Group Name", |
||
441 | ajax: { |
||
442 | url: 'ajax_list.php', |
||
443 | delay: 250, |
||
444 | data: function(params) { |
||
445 | return { |
||
446 | type: "transport_groups", |
||
447 | search: params.term |
||
448 | } |
||
449 | } |
||
450 | } |
||
451 | }); |
||
452 | </script> |
||
453 | <?php |
||
454 | } |
||
455 |
Let?s assume that you have a directory layout like this:
and let?s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/Foo.php
are loaded in the same runtime, you will see a PHP error such as the following:PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php
However, as
OtherDir/Foo.php
does not necessarily have to be loaded and the error is only triggered if it is loaded beforeOtherDir/Bar.php
, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias: