Files
TheWitnessandCopilot 2c7593c990 Guard maint maintenance-window checks against a missing maint functions file (#859)
* Guard maint maintenance-window checks against a missing maint functions file

#858 corrected the maint functions include path but left three
plugin_maint_check_cacti_host() calls guarded only by
api_plugin_is_enabled('maint'). When maint is enabled but its functions
file cannot be included (the include block now logs and continues rather
than fataling), those calls hit an undefined function and fatal the poll.

Add a function_exists('plugin_maint_check_cacti_host') guard to all three
call sites (thold_check_threshold() and both the recovering- and down-device
loops in thold_update_host_status()) so a missing maint include degrades
gracefully, matching the guard other plugins use.

Cover both maintenance-gated notification branches with a new
MaintCheckGuardTest; add an html_split_string() test stub so the
recovering-device path can be driven in the isolated unit process.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Let unexpected errors fail the maint-guard tests

Drop the broad catch(Throwable) in the host-status test helper. A device
inside a maintenance window is logged and the loop continues without the
downstream notification work, so the call returns on its own; removing the
catch means an unexpected error (such as a removed guard fataling on an
undefined maint function) now fails the test instead of being swallowed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 13:51:47 -04:00

11571 lines
391 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| 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 2 |
| 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. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDTool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
/**
* Return the CSP nonce attribute for inline <script> tags, safely across
* Cacti versions. Newer Cacti releases enforce a Content-Security-Policy that
* requires a per-request nonce on parser-inserted scripts; older releases lack
* the CactiSecureHeaders class, so this returns an empty string there.
*
* @return string The nonce attribute when supported, otherwise empty string.
*/
function plugin_thold_csp_nonce(): string {
if (class_exists('CactiSecureHeaders')) {
return CactiSecureHeaders::getNonceAttribute();
}
return '';
}
if (!defined('MESSAGE_LEVEL_NONE')) {
define('MESSAGE_LEVEL_NONE', 0);
define('MESSAGE_LEVEL_INFO', 1);
define('MESSAGE_LEVEL_WARN', 2);
define('MESSAGE_LEVEL_ERROR', 3);
define('MESSAGE_LEVEL_CSRF', 4);
}
if (!defined('STAT_HI')) {
define('STAT_HI', 2);
define('STAT_LO', 1);
define('STAT_NORMAL', 0);
}
if (!defined('THOLD_SEVERITY_NORMAL')) {
define('THOLD_SEVERITY_NORMAL', 0);
define('THOLD_SEVERITY_ALERT', 1);
define('THOLD_SEVERITY_WARNING', 2);
define('THOLD_SEVERITY_NOTICE', 3);
define('THOLD_SEVERITY_ACKREQ', 4);
define('THOLD_SEVERITY_DISABLED', 5);
define('THOLD_SEVERITY_BASELINE', 6);
}
/**
* sanitize_thold_sort_string - cleans up a search string submitted by the user to be passed
* to the database. NOTE: some of the code for this function came from the phpBB project.
*
* @param $string - the original raw search string
*
* @return - the sanitized search string
*/
function sanitize_thold_sort_string($string) {
static $drop_char_match = ['^', '$', '<', '>', '`', '\'', '"', '|', '?', '+', '[', ']', '{', '}', '#', ';', '!', '=', '*'];
static $drop_char_replace = [' ', ' ', ' ', ' ', '', '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '];
// Replace line endings by a space
$string = preg_replace('/[\n\r]/is', ' ', $string);
// HTML entities like &nbsp;
$string = preg_replace('/\b&[a-z]+;\b/', ' ', $string);
// Remove URL's
$string = preg_replace('/\b[a-z0-9]+:\/\/[a-z0-9\.\-]+(\/[a-z0-9\?\.%_\-\+=&\/]+)?/', ' ', $string);
// Filter out strange characters like ^, $, &, change "it's" to "its"
for ($i = 0; $i < cacti_count($drop_char_match); $i++) {
$string = str_replace($drop_char_match[$i], $drop_char_replace[$i], $string);
}
return $string;
}
/**
* get_time_since_last_event - renders a human-readable elapsed-time string describing how long a
* threshold has been in its current alert state.
*
* @param array $thold The threshold row, including 'instate' (state-change timestamp) and
* 'thold_alert' (alert type).
*
* @return string The elapsed time (or 'Never'/'Since Created'/'< 1 Minute') as applicable.
*/
function get_time_since_last_event($thold) {
$local_data_id = $thold['local_data_id'];
if (empty($thold['instate']) || $thold['instate'] < 60) {
return __esc('< 1 Minute', 'thold');
}
if (time() - $thold['instate'] < 1000) {
return __esc('Since Created', 'thold');
}
switch($thold['thold_alert']) {
case '0':
return get_daysfromtime($thold['instate']);
break;
case '1':
case '2':
return get_daysfromtime($thold['instate']);
break;
default:
return __('Never', 'thold');
}
}
/**
* thold_update_contacts - synchronizes the plugin_thold_contacts email entries from user_auth,
* inserting/updating one contact row per user with an email address and removing contacts for
* users that no longer exist or no longer have an email address.
*
* @return void
*/
function thold_update_contacts() {
$users = db_fetch_assoc("SELECT id, 'email' AS type, email_address
FROM user_auth
WHERE email_address != ''");
if (cacti_sizeof($users)) {
foreach ($users as $u) {
$cid = db_fetch_cell_prepared('SELECT id
FROM plugin_thold_contacts
WHERE type="email"
AND user_id = ?',
[$u['id']]);
if ($cid) {
db_execute_prepared('REPLACE INTO plugin_thold_contacts
(id, user_id, type, data)
VALUES (?, ?, "email", ?)',
[$cid, $u['id'], $u['email_address']]);
} else {
db_execute_prepared('REPLACE INTO plugin_thold_contacts
(user_id, type, data)
VALUES (?, "email", ?)',
[$u['id'], $u['email_address']]);
}
}
}
// cleanup old accounts
db_execute('DELETE ptc
FROM plugin_thold_contacts AS ptc
LEFT JOIN user_auth AS ua
ON ptc.user_id = ua.id
WHERE ua.email_address = ""
OR ua.id IS NULL');
}
/**
* thold_tabs - renders the tabbed navigation bar (Thresholds/Log/Device Status, plus any
* hook-registered tabs) shown at the top of thold_graph.php.
*
* @global array $config Cacti's global configuration array.
*
* @return void
*/
function thold_tabs() {
global $config;
// present a tabbed interface
$tabs = [
'thold' => __('Thresholds', 'thold'),
'log' => __('Log', 'thold'),
'hoststat' => __('Device Status', 'thold')
];
$tabs = api_plugin_hook_function('thold_graph_tabs', $tabs);
get_filter_request_var('tab', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^([a-zA-Z]+)$/']]);
load_current_session_value('tab', 'sess_thold_graph_tab', 'general');
$current_tab = get_request_var('action');
// draw the tabs
print "<div class='tabs'><nav><ul>\n";
if (cacti_sizeof($tabs)) {
foreach (array_keys($tabs) as $tab_short_name) {
print "<li><a class='tab" . (($tab_short_name == $current_tab) ? " selected'" : "'") .
" href='" . html_escape($config['url_path'] .
'plugins/thold/thold_graph.php?' .
'action=' . $tab_short_name) .
"'>" . $tabs[$tab_short_name] . "</a></li>\n";
}
}
print "</ul></nav></div>\n";
}
/**
* thold_debug - logs a message via thold_cacti_log() only when the 'thold_log_debug' setting or
* the global debug flag is enabled.
*
* @param string $txt The message to log.
* @param string $thread An optional thread/context identifier passed through to the logger.
*
* @global bool $debug Whether debug logging is enabled for this request.
*
* @return void
*/
function thold_debug($txt, $thread = '') {
global $debug;
if (read_config_option('thold_log_debug') == 'on' || $debug) {
thold_cacti_log($txt, $thread, true);
}
}
/**
* thold_template_avail_devices - builds a SQL WHERE-clause fragment restricting a device list to
* hosts that are still eligible to have a threshold created from the given (or, if none is
* given, any) threshold template - i.e. hosts using a graph template the template applies to that
* either allow multiple graphs or don't already have one.
*
* @param int $thold_template_id The threshold template id to check, or 0 for any template.
*
* @return string A `h.id IN (...)` SQL fragment, or an empty string when no devices qualify.
*/
function thold_template_avail_devices($thold_template_id = 0) {
// display the host dropdown
if ($thold_template_id > 0) {
$graph_templates = array_rekey(db_fetch_assoc_prepared('SELECT DISTINCT gt.id
FROM graph_templates AS gt
INNER JOIN graph_templates_item AS gti
ON gt.id=gti.graph_template_id
AND local_graph_id=0
INNER JOIN data_template_rrd AS dtr
ON dtr.id=task_item_id
INNER JOIN thold_template AS tt
ON tt.data_template_id=dtr.data_template_id
INNER JOIN thold_data AS td
ON td.data_template_id=dtr.data_template_id
AND gt.id=td.graph_template_id
AND tt.id = ?',
[$thold_template_id]), 'id', 'id');
} else {
$graph_templates = array_rekey(db_fetch_assoc('SELECT DISTINCT gt.id
FROM graph_templates AS gt
INNER JOIN graph_templates_item AS gti
ON gt.id=gti.graph_template_id
AND local_graph_id=0
INNER JOIN data_template_rrd AS dtr
ON dtr.id=task_item_id
INNER JOIN thold_template AS tt
ON tt.data_template_id=dtr.data_template_id
INNER JOIN thold_data AS td
ON td.data_template_id=dtr.data_template_id
AND gt.id=td.graph_template_id'), 'id', 'id');
}
// Limit the hosts to only hosts that either have a graph template
// Listed as multiple, or do not have a threshold created
// Using the Graph Template listed
$device_ids = [];
if (cacti_sizeof($graph_templates)) {
$device_ids = array_rekey(db_fetch_assoc('SELECT DISTINCT rs.id
FROM (
SELECT h.id, gt.id AS gti, gt.multiple
FROM host AS h,graph_templates AS gt
) AS rs
LEFT JOIN graph_local AS gl
ON gl.graph_template_id=rs.gti
AND gl.host_id=rs.id
WHERE (gti IN(' . implode(', ', $graph_templates) . ')
AND host_id IS NULL)
OR rs.multiple = "on"'), 'id', 'id');
}
return (cacti_sizeof($device_ids) ? 'h.id IN (' . implode(', ', $device_ids) . ')' : '');
}
/**
* thold_initialize_rusage - captures a baseline resource-usage snapshot (and start time) into a
* global, for later comparison by thold_display_rusage().
*
* @global array $thold_start_rusage The captured baseline getrusage() data plus start microtime.
*
* @return void
*/
function thold_initialize_rusage() {
global $thold_start_rusage;
if (function_exists('getrusage')) {
$thold_start_rusage = getrusage();
}
$thold_start_rusage['microtime'] = microtime(true);
}
/**
* thold_display_rusage - prints a one-row table of elapsed wall-clock time, user/system CPU time,
* swaps, and page faults since thold_initialize_rusage() was last called.
*
* @global array $thold_start_rusage The baseline captured by thold_initialize_rusage().
*
* @return void
*/
function thold_display_rusage() {
global $thold_start_rusage;
if (function_exists('getrusage')) {
$dat = getrusage();
html_start_box('', '100%', false, '3', 'left', '');
print '<tr>';
if (!isset($thold_start_rusage)) {
print "<td colspan='10'>ERROR: Can not display RUSAGE please call thold_initialize_rusage first</td>";
} else {
$i_u_time = $thold_start_rusage['ru_utime.tv_sec'] + ($thold_start_rusage['ru_utime.tv_usec'] * 1E-6);
$i_s_time = $thold_start_rusage['ru_stime.tv_sec'] + ($thold_start_rusage['ru_stime.tv_usec'] * 1E-6);
$s_s = $thold_start_rusage['ru_nswap'];
$s_pf = $thold_start_rusage['ru_majflt'];
$start_time = $thold_start_rusage['microtime'];
$end_time = microtime(true);
$utime = ($dat['ru_utime.tv_sec'] + ($dat['ru_utime.tv_usec'] * 1E-6)) - $i_u_time;
$stime = ($dat['ru_stime.tv_sec'] + ($dat['ru_stime.tv_usec'] * 1E-6)) - $i_s_time;
$swaps = $dat['ru_nswap'] - $s_s;
$pages = $dat['ru_majflt'] - $s_pf;
print "<td colspan='10' width='1%' style='text-align:left;'>";
print '<b>' . __('Time:', 'thold') . '</b>&nbsp;' . round($end_time - $start_time,2) . ' seconds, ';
print '<b>' . __('User:', 'thold') . '</b>&nbsp;' . round($utime,2) . ' seconds, ';
print '<b>' . __('System:', 'thold') . '</b>&nbsp;' . round($stime,2) . ' seconds, ';
print '<b>' . __('Swaps:', 'thold') . '</b>&nbsp;' . ($swaps) . ' swaps, ';
print '<b>' . __('Pages:', 'thold') . '</b>&nbsp;' . ($pages) . ' pages';
print '</td>';
}
print '</tr>';
html_end_box(false);
}
}
/**
* thold_legend - renders a legend row showing every threshold state's display label styled with
* its own CSS class.
*
* @global array $thold_states The threshold state definitions (class/display per state).
*
* @return void
*/
function thold_legend() {
global $thold_states;
html_start_box('', '100%', false, '3', 'center', '');
$chip_min = 0;
foreach ($thold_states as $state) {
$chip_min = max($chip_min, mb_strlen($state['display']));
}
print '<tr class="tableRow"><td>';
print '<div class="tholdLegend" style="--thold-chip-min: calc(' . $chip_min . 'ch + 1.5rem)">';
foreach ($thold_states as $index => $state) {
print '<div class="tholdLegendItem ' . $state['class'] . '">' . html_escape($state['display']) . '</div>';
}
print '</div>';
print '</td></tr>';
html_end_box(false);
}
/**
* host_legend - renders a legend row showing every device-status state's display label styled
* with its own CSS class.
*
* @global array $thold_host_states The device-status state definitions (class/display per state).
*
* @return void
*/
function host_legend() {
global $thold_host_states;
html_start_box('', '100%', false, '3', 'center', '');
$chip_min = 0;
foreach ($thold_host_states as $state) {
$chip_min = max($chip_min, mb_strlen($state['display']));
}
print '<tr class="tableRow"><td>';
print '<div class="tholdLegend" style="--thold-chip-min: calc(' . $chip_min . 'ch + 1.5rem)">';
foreach ($thold_host_states as $index => $state) {
print '<div class="tholdLegendItem ' . $state['class'] . '">' . html_escape($state['display']) . '</div>';
}
print '</div>';
print '</td></tr>';
html_end_box(false);
}
/**
* log_legend - renders a legend row showing every log-state's short display label styled with
* its own CSS class.
*
* @global array $thold_log_states The log state definitions (class/display_short per state).
*
* @return void
*/
function log_legend() {
global $thold_log_states;
html_start_box('', '100%', false, '3', 'center', '');
$chip_min = 0;
foreach ($thold_log_states as $state) {
$chip_min = max($chip_min, mb_strlen($state['display_short']));
}
print '<tr class="tableRow"><td>';
print '<div class="tholdLegend" style="--thold-chip-min: calc(' . $chip_min . 'ch + 1.5rem)">';
foreach ($thold_log_states as $index => $state) {
print '<div class="tholdLegendItem ' . $state['class'] . '">' . html_escape($state['display_short']) . '</div>';
}
print '</div>';
print '</td></tr>';
html_end_box(false);
}
/**
* thold_expression_rpn_pop - pops and returns the top value off an RPN evaluation stack,
* flagging the shared RPN error state when the stack is empty.
*
* @param array &$stack The RPN operand stack, modified in place.
*
* @global bool $rpn_error Set to true when the stack is unexpectedly empty.
*
* @return mixed The popped value, or false if the stack was empty.
*/
function thold_expression_rpn_pop(&$stack) {
global $rpn_error;
if (cacti_sizeof($stack)) {
return array_pop($stack);
} else {
$rpn_error = true;
return false;
}
}
/**
* thold_rpn_math_binary - safely evaluates a binary arithmetic operation
* without using eval(). The operator must be one of the whitelisted RPN
* math tokens (+, -, *, /, %, ^).
*
* @param string $operator The arithmetic operator
* @param mixed $v2 The left operand (validated numeric)
* @param mixed $v1 The right operand (validated numeric)
*
* @return mixed The result of the operation
*/
function thold_rpn_math_binary($operator, $v2, $v1) {
global $rpn_error;
switch ($operator) {
case '+':
return $v2 + $v1;
case '-':
return $v2 - $v1;
case '*':
return $v2 * $v1;
case '/':
return $v2 / $v1;
case '%':
return (int) $v2 % (int) $v1;
case '^':
// Bitwise XOR, not exponentiation: eval('$v3 = ' . $v2 . ' ^ ' . $v1 . ';')
// always computed XOR (PHP's ^ operator), and existing user thresholds
// rely on that. See TholdExpressionMathRpnTest::testCaretOperatorIsIntegerXorNotExponentiation.
return (int) $v2 ^ (int) $v1;
default:
cacti_log("ERROR: RPN unknown binary operator '$operator'", false, 'THOLD');
$rpn_error = true;
return 0;
}
}
/**
* thold_rpn_math_unary - safely evaluates a unary math function
* without using eval(). The operator must be one of the whitelisted
* RPN unary math function names (SIN, COS, TAN, ATAN, SQRT, FLOOR,
* CEIL, DEG2RAD, RAD2DEG, ABS, EXP, LOG).
*
* @param string $operator The function name
* @param mixed $v1 The operand (validated numeric)
*
* @return mixed The result of the function call
*/
function thold_rpn_math_unary($operator, $v1) {
global $rpn_error;
switch ($operator) {
case 'SIN':
return sin($v1);
case 'COS':
return cos($v1);
case 'TAN':
return tan($v1);
case 'ATAN':
return atan($v1);
case 'SQRT':
return sqrt($v1);
case 'FLOOR':
return floor($v1);
case 'CEIL':
return ceil($v1);
case 'DEG2RAD':
return deg2rad($v1);
case 'RAD2DEG':
return rad2deg($v1);
case 'ABS':
return abs($v1);
case 'EXP':
return exp($v1);
case 'LOG':
return log($v1);
default:
cacti_log("ERROR: RPN unknown unary operator '$operator'", false, 'THOLD');
$rpn_error = true;
return 0;
}
}
/**
* thold_expression_math_rpn - applies one binary/unary RPN math operator (+, -, *, /, %, ^, the
* trig/rounding unary functions, ATAN2, ADDNAN) to the top of the evaluation stack, validating
* operands and flagging the shared RPN error state on invalid input.
*
* @param string $operator The RPN math token being applied.
* @param array &$stack The RPN evaluation stack, modified in place.
*
* @global bool $rpn_error Set to true when an operand is invalid or undefined.
*
* @return void
*/
function thold_expression_math_rpn($operator, &$stack) {
global $rpn_error;
$orig_stack = $stack;
switch($operator) {
case '+':
case '-':
case '/':
case '*':
case '%':
case '^':
$v1 = thold_expression_rpn_pop($stack);
$v2 = thold_expression_rpn_pop($stack);
$v3 = 'U';
$rpn_evaled = false;
if (!is_numeric($v1)) {
cacti_log('ERROR: RPN value: v1 "' . $v1 . '" is Not valid for operator "' . $operator . '". Stack:"' . implode(',', $orig_stack) . '"', false, 'THOLD');
$rpn_error = true;
} elseif (!is_numeric($v2)) {
cacti_log('ERROR: RPN value: v2 "' . $v2 . '" is Not valid for operator "' . $operator . '". Stack:"' . implode(',', $orig_stack) . '"', false, 'THOLD');
$rpn_error = true;
} elseif ($v1 == 0 && $v2 == 0 && $operator == '/') {
// Not a loop/switch context: this only exits the if/elseif
// chain below, it must not use "break" (which would exit the
// enclosing switch($operator) and skip the array_push below).
$v3 = 0;
$rpn_evaled = true;
} elseif ($v1 == 0 && $operator == '/') {
cacti_log('ERROR: RPN value: v1 can not be "0" when the operator is "' . $operator . '". Stack:"' . implode(',', $orig_stack) . '"', false, 'THOLD');
$rpn_error = true;
} elseif ($operator == '%' && (int) $v1 == 0) {
// The % operator truncates both operands to int, so a fractional
// divisor such as 0.5 becomes 0 even though $v1 == 0 is false.
cacti_log('ERROR: RPN value: v1 can not be "0" when the operator is "' . $operator . '". Stack:"' . implode(',', $orig_stack) . '"', false, 'THOLD');
$rpn_error = true;
}
if ($rpn_evaled) {
array_push($stack, $v3);
} elseif (!$rpn_error) {
$v3 = thold_rpn_math_binary($operator, $v2, $v1);
if ($v3 == '') {
$v3 = 0;
}
array_push($stack, $v3);
}
break;
case 'SIN':
case 'COS':
case 'TAN':
case 'ATAN':
case 'SQRT':
case 'FLOOR':
case 'CEIL':
case 'DEG2RAD':
case 'RAD2DEG':
case 'ABS':
case 'EXP':
case 'LOG':
$v1 = thold_expression_rpn_pop($stack);
if (!$rpn_error && !is_numeric($v1)) {
cacti_log('ERROR: RPN value: v1 "' . $v1 . '" is Not valid for operator "' . $operator . '". Stack:"' . implode(',', $orig_stack) . '"', false, 'THOLD');
$rpn_error = true;
}
if (!$rpn_error) {
$v2 = thold_rpn_math_unary($operator, $v1);
if (is_nan($v2) || is_infinite($v2)) {
cacti_log('ERROR: RPN value: result of "' . $operator . '(' . $v1 . ')" is undefined. Stack:"' . implode(',', $orig_stack) . '"', false, 'THOLD');
$rpn_error = true;
} else {
array_push($stack, $v2);
}
}
break;
case 'ATAN2':
$v1 = thold_expression_rpn_pop($stack);
$v2 = thold_expression_rpn_pop($stack);
if (!$rpn_error) {
$v3 = atan2($v1, $v2);
array_push($stack, $v3);
}
break;
case 'ADDNAN':
$v1 = thold_expression_rpn_pop($stack);
$v2 = thold_expression_rpn_pop($stack);
if (!$rpn_error) {
if ($v1 == 'NAN' || $v1 == 'U') {
$v1 = 0;
}
if ($v2 == 'NAN' || $v2 == 'U') {
$v2 = 0;
}
array_push($stack, $v1 + $v2);
}
break;
}
}
/**
* thold_expression_boolean_rpn - applies one boolean/comparison RPN operator (UN, ISINF, AND, OR,
* IF, LT, GT, LE, GE, EQ, NE) to the top of the evaluation stack, pushing '1'/'0' (or the
* selected branch for IF).
*
* @param string $operator The RPN boolean/comparison token being applied.
* @param array &$stack The RPN evaluation stack, modified in place.
*
* @global bool $rpn_error The shared RPN error state (not modified here).
*
* @return void
*/
function thold_expression_boolean_rpn($operator, &$stack) {
global $rpn_error;
if ($operator == 'UN') {
$v1 = thold_expression_rpn_pop($stack);
if ($v1 == 'U' || $v1 == 'NAN') {
array_push($stack, '1');
} else {
array_push($stack, '0');
}
} elseif ($operator == 'ISINF') {
$v1 = thold_expression_rpn_pop($stack);
if ($v1 == 'INF' || $v1 == 'NEGINF') {
array_push($stack, '1');
} else {
array_push($stack, '0');
}
} elseif ($operator == 'AND') {
$v1 = thold_expression_rpn_pop($stack);
$v2 = thold_expression_rpn_pop($stack);
if ($v1 > 0 && $v2 > 0) {
array_push($stack, '1');
} else {
array_push($stack, '0');
}
} elseif ($operator == 'OR') {
$v1 = thold_expression_rpn_pop($stack);
$v2 = thold_expression_rpn_pop($stack);
if ($v1 > 0 || $v2 > 0) {
array_push($stack, '1');
} else {
array_push($stack, '0');
}
} elseif ($operator == 'IF') {
$v1 = thold_expression_rpn_pop($stack);
$v2 = thold_expression_rpn_pop($stack);
$v3 = thold_expression_rpn_pop($stack);
if ($v3 == 0) {
array_push($stack, $v1);
} else {
array_push($stack, $v2);
}
} else {
$v2 = thold_expression_rpn_pop($stack);
$v1 = thold_expression_rpn_pop($stack);
// deal with unknown or infinite data
if (($v1 == 'INF' || $v2 == 'INF') ||
($v1 == 'NAN' || $v2 == 'NAN') ||
($v1 == 'U' || $v2 == 'U') ||
($v1 == 'NEGINF' || $v2 == 'NEGINF')) {
array_push($stack, '0');
} else {
switch($operator) {
case 'LT':
if ($v1 < $v2) {
array_push($stack, '1');
} else {
array_push($stack, '0');
}
break;
case 'GT':
if ($v1 > $v2) {
array_push($stack, '1');
} else {
array_push($stack, '0');
}
break;
case 'LE':
if ($v1 <= $v2) {
array_push($stack, '1');
} else {
array_push($stack, '0');
}
break;
case 'GE':
if ($v1 >= $v2) {
array_push($stack, '1');
} else {
array_push($stack, '0');
}
break;
case 'EQ':
if ($v1 == $v2) {
array_push($stack, '1');
} else {
array_push($stack, '0');
}
break;
case 'NE':
if ($v1 != $v2) {
array_push($stack, '1');
} else {
array_push($stack, '0');
}
break;
}
}
}
}
/**
* thold_expression_compare_rpn - applies one RPN comparison/selection operator (MAX, MIN, or the
* 3-value LIMIT-style range check) to the top of the evaluation stack.
*
* @param string $operator The RPN token being applied (MAX, MIN, or the range-check form).
* @param array &$stack The RPN evaluation stack, modified in place.
*
* @global bool $rpn_error The shared RPN error state (not modified here).
*
* @return void
*/
function thold_expression_compare_rpn($operator, &$stack) {
global $rpn_error;
if ($operator == 'MAX' || $operator == 'MIN') {
$v[0] = thold_expression_rpn_pop($stack);
$v[1] = thold_expression_rpn_pop($stack);
if (in_array('INF', $v, true)) {
array_push($stack, 'INF');
} elseif (in_array('NEGINF', $v, true)) {
array_push($stack, 'NEGINF');
} elseif (in_array('U', $v, true)) {
array_push($stack, 'U');
} elseif (in_array('NAN', $v, true)) {
array_push($stack, 'NAN');
} elseif ($operator == 'MAX') {
array_push($stack, max($v));
} else {
array_push($stack, min($v));
}
} else {
$v1 = thold_expression_rpn_pop($stack);
$v2 = thold_expression_rpn_pop($stack);
$v3 = thold_expression_rpn_pop($stack);
if (($v1 == 'U' || $v1 == 'NAN') ||
($v2 == 'U' || $v2 == 'NAN') ||
($v3 == 'U' || $v3 == 'NAN')) {
array_push($stack, 'U');
} elseif (($v1 == 'INF' || $v1 == 'NEGINF') ||
($v2 == 'INF' || $v2 == 'NEGINF') ||
($v3 == 'INF' || $v3 == 'NEGINF')) {
array_push($stack, 'U');
} elseif ($v1 < $v2) {
if ($v3 >= $v1 && $v3 <= $v2) {
array_push($stack, $v3);
} else {
array_push($stack, 'U');
}
} else {
if ($v3 >= $v2 && $v3 <= $v1) {
array_push($stack, $v3);
} else {
array_push($stack, 'U');
}
}
}
}
/**
* thold_expression_specvals_rpn - pushes an RPN special-value token (UNKN, INF, NEGINF, COUNT) or
* a placeholder for PREV (not yet implemented) onto the evaluation stack.
*
* @param string $operator The special-value token being applied.
* @param array &$stack The RPN evaluation stack, modified in place.
* @param int $count The current CDEF sample count, pushed for the COUNT token.
*
* @global bool $rpn_error The shared RPN error state (not modified here).
*
* @return void
*/
function thold_expression_specvals_rpn($operator, &$stack, $count) {
global $rpn_error;
if ($operator == 'UNKN') {
array_push($stack, 'U');
} elseif ($operator == 'INF') {
array_push($stack, 'INF');
} elseif ($operator == 'NEGINF') {
array_push($stack, 'NEGINF');
} elseif ($operator == 'COUNT') {
array_push($stack, $count);
} elseif ($operator == 'PREV') {
// still have to figure this out
}
}
/**
* Apply one stack-manipulation RPN operator.
*
* @param string $operator DUP, POP or EXC.
* @param array<int,mixed> $stack Evaluation stack, modified in place.
*/
function thold_expression_stackops_rpn(string $operator, array &$stack): void {
global $rpn_error;
switch ($operator) {
case 'DUP':
$v1 = thold_expression_rpn_pop($stack);
if ($rpn_error) {
return;
}
array_push($stack, $v1, $v1);
break;
case 'POP':
thold_expression_rpn_pop($stack);
break;
default:
// EXC. Popping already reverses the pair, so push them back in pop order.
$v1 = thold_expression_rpn_pop($stack);
$v2 = thold_expression_rpn_pop($stack);
if ($rpn_error) {
return;
}
array_push($stack, $v1, $v2);
break;
}
}
/**
* thold_expression_time_rpn - applies an RPN time operator; only NOW (push the current Unix
* timestamp) is currently implemented, TIME/LTIME are reserved for future support.
*
* @param string $operator The RPN time token being applied.
* @param array &$stack The RPN evaluation stack, modified in place.
*
* @global bool $rpn_error The shared RPN error state (not modified here).
*
* @return void
*/
function thold_expression_time_rpn($operator, &$stack) {
global $rpn_error;
if ($operator == 'NOW') {
array_push($stack, time());
} elseif ($operator == 'TIME') {
// still need to figure this one out
} elseif ($operator == 'LTIME') {
// still need to figure this one out
}
}
/**
* Apply one set RPN operator to the top $count elements of the stack.
*
* SORT, REV and AVG each pop a count first. AVG follows rrdtool: unknown
* samples are skipped rather than poisoning the sum, and an all-unknown span
* yields UNKN.
*
* @param string $operator SORT, REV or AVG.
* @param array<int,mixed> $stack Evaluation stack, modified in place.
*/
function thold_expression_setops_rpn(string $operator, array &$stack): void {
global $rpn_error;
if (!in_array($operator, ['SORT', 'REV', 'AVG'], true)) {
return;
}
$count = thold_expression_rpn_pop($stack);
if ($rpn_error || !is_numeric($count) || $count <= 0) {
return;
}
if ((float) $count !== floor((float) $count)) {
$rpn_error = true;
return;
}
$count = (int) $count;
// Popping yields the span top-down; keep it that way and index deliberately.
$values = [];
for ($i = 0; $i < $count; $i++) {
$values[] = thold_expression_rpn_pop($stack);
}
if ($rpn_error) {
return;
}
if ($operator === 'SORT') {
sort($values, SORT_NUMERIC);
}
// REV needs no work: $values is already reversed relative to the stack.
if ($operator !== 'AVG') {
foreach ($values as $value) {
$stack[] = $value;
}
return;
}
$total = 0;
$known = 0;
foreach ($values as $value) {
if ($value == 'INF' || $value == 'NEGINF') {
$stack[] = $value == 'INF' ? 'INF' : 'NEGINF';
return;
}
if (!is_numeric($value)) {
continue;
}
$total += $value;
$known++;
}
$stack[] = $known === 0 ? 'U' : $total / $known;
}
/**
* thold_expression_ds_value - pushes the current value of the named data source (case-insensitive
* match on RRD field name) onto the evaluation stack, or 0 if it isn't present in this poll's
* data.
*
* @param string $operator The RRD data-source (field) name being looked up.
* @param array &$stack The RPN evaluation stack, modified in place.
* @param array $data_sources RRD field name => current value map for this poll cycle.
*
* @global bool $rpn_error The shared RPN error state (not modified here).
*
* @return void
*/
function thold_expression_ds_value($operator, &$stack, $data_sources) {
global $rpn_error;
if (cacti_sizeof($data_sources)) {
foreach ($data_sources as $rrd_name => $value) {
if (strtoupper($rrd_name) == $operator) {
array_push($stack, $value);
return;
}
}
}
array_push($stack, 0);
}
/**
* thold_expression_specialtype_rpn - pushes a Cacti CDEF special-property value (current data
* source, graph/data-source min/max, total HDD, or the sum of all data sources for the local
* data set) onto the evaluation stack.
*
* @param string $operator The special-property token being applied.
* @param array &$stack The RPN evaluation stack, modified in place.
* @param int $local_data_id The local data source id the special value is computed against.
* @param mixed $currentval The current data source's already-resolved value.
*
* @return void
*/
function thold_expression_specialtype_rpn($operator, &$stack, $local_data_id, $currentval) {
switch ($operator) {
case 'CURRENT_DATA_SOURCE':
array_push($stack, $currentval);
break;
case 'CURRENT_GRAPH_MAXIMUM_VALUE':
array_push($stack, get_current_value($local_data_id, 'upper_limit'));
break;
case 'CURRENT_GRAPH_MINIMUM_VALUE':
array_push($stack, get_current_value($local_data_id, 'lower_limit'));
break;
case 'CURRENT_DS_MINIMUM_VALUE':
array_push($stack, get_current_value($local_data_id, 'rrd_minimum'));
break;
case 'CURRENT_DS_MAXIMUM_VALUE':
array_push($stack, get_current_value($local_data_id, 'rrd_maximum'));
break;
case 'VALUE_OF_HDD_TOTAL':
array_push($stack, get_current_value($local_data_id, 'hdd_total'));
break;
case 'ALL_DATA_SOURCES_NODUPS':
case 'ALL_DATA_SOURCES_DUPS':
$v1 = 0;
$all_dsns = db_fetch_assoc_prepared('SELECT data_source_name
FROM data_template_rrd
WHERE local_data_id = ?',
[$local_data_id]);
if (cacti_sizeof($all_dsns)) {
foreach ($all_dsns as $dsn) {
$v1 += get_current_value($local_data_id, $dsn['data_source_name']);
}
}
array_push($stack, $v1);
break;
default:
cacti_log('WARNING: CDEF property not implemented yet: ' . $operator, false, 'THOLD', POLLER_VERBOSITY_MEDIUM);
array_push($stack, $currentval);
break;
}
}
/**
* Counts a wrapped counter has advanced by, given the previous and current
* readings.
*
* The modulus is 2^32 or 2^64, not one less than it, so the previous code lost
* exactly one count per wrap. 2^64 is above PHP_INT_MAX and would be parsed as
* a float, losing about eleven bits at that magnitude, so the 64-bit case goes
* through GMP. Cacti already requires ext-gmp.
*
* @param float|int|string $oldvalue Previous reading.
* @param float|int|string $newvalue Current reading.
*
* @return float|int
*/
function thold_counter_wrap_delta($oldvalue, $newvalue) {
if ($oldvalue > 4294967295) {
$old_integer = trim((string) $oldvalue);
$new_integer = trim((string) $newvalue);
if (!preg_match('/^\d+$/D', $old_integer) || !preg_match('/^\d+$/D', $new_integer)) {
return (18446744073709551616.0 - (float) $oldvalue) + (float) $newvalue;
}
$delta = gmp_add(gmp_sub(gmp_pow(2, 64), gmp_init($old_integer, 10)), gmp_init($new_integer, 10));
return (float) gmp_strval($delta);
}
return (4294967296 - $oldvalue) + $newvalue;
}
/**
* Whether a valid current sample predates the stored sample clock.
*
* @param array<string,mixed> $thold_data
* @param array<string,mixed> $item
* @param int $currenttime
*
* @return bool
*/
function thold_sample_clock_moved_backward(array $thold_data, array $item, $currenttime) {
$name = (string) ($thold_data['name'] ?? '');
$currenttime = (int) $currenttime;
$lasttime = (int) ($thold_data['lasttime'] ?? 0);
return $name !== ''
&& $currenttime > 0
&& $lasttime > 0
&& $currenttime < $lasttime
&& isset($item[$name])
&& is_numeric($item[$name]);
}
/**
* Whether the interval ending at $currenttime is within the effective
* heartbeat thold_get_currentval() uses to trust a counter/derive/absolute
* baseline. thold_sample_persistence() uses this only to decide whether to
* log a stale-gap re-anchor; the pair itself always advances.
*
* The caller must already know $currenttime is strictly after a real prior
* sample (lasttime > 0, currenttime > lasttime); this only computes the
* heartbeat comparison thold_get_currentval() applies in that situation.
*
* @param array<string,mixed> $thold_data
* @param int $currenttime
*
* @return bool
*/
function thold_sample_interval_eligible(array $thold_data, $currenttime) {
$elapsed = ((int) $currenttime) - (int) ($thold_data['lasttime'] ?? 0);
$poller_interval = read_config_option('poller_interval');
if (!is_numeric($poller_interval) || $poller_interval <= 0) {
$poller_interval = 300;
}
$rrd_step = is_numeric($thold_data['rrd_step'] ?? null) && $thold_data['rrd_step'] > 0
? (float) $thold_data['rrd_step']
: 0.0;
$sample_interval = max($rrd_step, (float) $poller_interval);
$rrd_heartbeat = is_numeric($thold_data['rrd_heartbeat'] ?? null) && $thold_data['rrd_heartbeat'] > 0
? max((float) $thold_data['rrd_heartbeat'], 2 * $sample_interval)
: 2 * $sample_interval;
return $elapsed <= $rrd_heartbeat;
}
/**
* Persist a raw sample and its timestamp as one causal pair.
*
* `$thold_data` must provide `name`, `lasttime`, and `oldvalue`; absent values
* fail closed to an unavailable prior sample. The pair always re-anchors to
* the current sample when one is available: thold_get_currentval() already
* rejected the rate for this cycle using the pair as it stood before this
* call, so freezing lasttime/oldvalue here would buy no extra protection for
* the current read while permanently disqualifying every later poll, whose
* elapsed time is measured against that same frozen lasttime and only grows.
* Re-anchoring instead lets the very next poll compute a fresh, valid rate.
*
* @param array<string,mixed> $thold_data
* @param array<string,mixed> $item
* @param int $currenttime
*
* @return array{lasttime:mixed,oldvalue:mixed}
*/
function thold_sample_persistence(array $thold_data, array $item, $currenttime) {
$name = (string) ($thold_data['name'] ?? '');
$currenttime = (int) $currenttime;
$lasttime = (int) ($thold_data['lasttime'] ?? 0);
if ($name !== '' && $currenttime > 0 && $currenttime !== $lasttime && isset($item[$name]) && is_numeric($item[$name])) {
$is_rate_bearing = in_array((int) ($thold_data['data_source_type_id'] ?? 0), [2, 3, 4], true);
$clock_moved_back = thold_sample_clock_moved_backward($thold_data, $item, $currenttime);
if ($clock_moved_back) {
cacti_log(sprintf(
'WARNING: Threshold %s sample clock moved backwards; re-anchoring its value and timestamp.',
$thold_data['id'] ?? ($thold_data['thold_id'] ?? 'unknown')
), false, 'THOLD', POLLER_VERBOSITY_MEDIUM);
} elseif ($lasttime > 0 && $is_rate_bearing && !thold_sample_interval_eligible($thold_data, $currenttime) && is_numeric($thold_data['lastread'] ?? null)) {
// Only the poll that first crosses the heartbeat logs: once this
// source is already persisted as unavailable, every later poll
// re-anchors again (its own gap is now measured from here) but
// stays quiet, or a persistently flaky device would flood the
// log at medium verbosity.
cacti_log(sprintf(
'WARNING: Threshold %s sample gap exceeded the effective heartbeat; re-anchoring its rate baseline so the next poll can recover.',
$thold_data['id'] ?? ($thold_data['thold_id'] ?? 'unknown')
), false, 'THOLD', POLLER_VERBOSITY_MEDIUM);
}
return ['lasttime' => $currenttime, 'oldvalue' => $item[$name]];
}
return [
'lasttime' => $lasttime,
'oldvalue' => $thold_data['oldvalue'] ?? null,
];
}
/**
* Log only the transition from a numeric result to an unavailable result.
*
* @param array<string,mixed> $thold_data
* @param mixed $currentval
*
* @return void
*/
function thold_log_unavailable_transition(array $thold_data, $currentval) {
if (is_numeric($currentval) || !is_numeric($thold_data['lastread'] ?? null)) {
return;
}
cacti_log(sprintf(
'WARNING: Threshold %s (%s) current sample is unavailable; preserving its alert state.',
$thold_data['id'] ?? ($thold_data['thold_id'] ?? 'unknown'),
$thold_data['name_cache'] ?? ($thold_data['thold_name'] ?? ($thold_data['name'] ?? 'unknown'))
), false, 'THOLD', POLLER_VERBOSITY_MEDIUM);
}
/**
* Persist one daemon sample without manufacturing a zero SQL timestamp.
*
* @param array<string,mixed> $thold_data
* @param array<string,mixed> $item
* @param mixed $currentval
* @param int $currenttime
*
* @return bool
*/
function thold_daemon_persist_sample(array $thold_data, array $item, $currentval, $currenttime) {
$id = (int) ($thold_data['thold_id'] ?? 0);
$tcheck = 1;
if ($id <= 0) {
return false;
}
$sample = thold_sample_persistence($thold_data, $item, $currenttime);
if (!thold_sample_clock_moved_backward($thold_data, $item, $currenttime)) {
thold_log_unavailable_transition($thold_data, $currentval);
}
if ($sample['lasttime'] <= 0) {
return db_execute_prepared('UPDATE thold_data
SET tcheck = ?, lastread = ?
WHERE id = ?',
[$tcheck, $currentval, $id]);
}
return db_execute_prepared('UPDATE thold_data
SET tcheck = ?, lastread = ?,
lasttime = FROM_UNIXTIME(?), oldvalue = ?
WHERE id = ?',
[$tcheck, $currentval, $sample['lasttime'], $sample['oldvalue'], $id]);
}
/**
* Build one pure poller batching result for sample or status-only updates.
*
* @param array<string,mixed> $thold_data
* @param array<string,mixed> $item
* @param mixed $currentval
* @param int $currenttime
*
* @return array{sample_row:array{id:int,tcheck:int,lastread:mixed,lasttime:mixed,oldvalue:mixed}|null,status_row:array{id:int,tcheck:int,lastread:mixed}|null}
*/
function thold_polling_sample_row(array $thold_data, array $item, $currentval, $currenttime) {
$id = (int) ($thold_data['id'] ?? 0);
$tcheck = 1;
if ($id <= 0) {
return ['sample_row' => null, 'status_row' => null];
}
$sample = thold_sample_persistence($thold_data, $item, $currenttime);
if (!thold_sample_clock_moved_backward($thold_data, $item, $currenttime)) {
thold_log_unavailable_transition($thold_data, $currentval);
}
if ($sample['lasttime'] <= 0) {
return [
'sample_row' => null,
'status_row' => ['id' => $id, 'tcheck' => $tcheck, 'lastread' => $currentval],
];
}
return [
'sample_row' => [
'id' => $id,
'tcheck' => $tcheck,
'lastread' => $currentval,
'lasttime' => $sample['lasttime'],
'oldvalue' => $sample['oldvalue'],
],
'status_row' => null,
];
}
/**
* Remove rows deleted during polling after either update batch ran.
*
* @param bool $has_updates
*
* @return void
*/
function thold_polling_cleanup($has_updates) {
if (!$has_updates) {
return;
}
db_execute_prepared('DELETE FROM thold_data WHERE local_data_id = 0');
if (db_affected_rows() > 0) {
set_config_option('time_last_change_thold', time());
}
}
/**
* thold_get_currentval - resolves a threshold's current sample value from this cycle's reindexed
* poller output, applying the correct COUNTER/DERIVE/ABSOLUTE/GAUGE rate calculation (including
* counter-wrap handling and interface-speed-derived maximums) based on the elapsed step since the
* last usable sample.
*
* @param array &$thold_data The threshold's data-source row (lasttime/oldvalue/rrd_* fields).
* @param array &$rrd_reindexed local_data_id => (rrd field name => value) map for this poll cycle.
* @param array &$rrd_time_reindexed local_data_id => sample Unix timestamp map for this poll cycle.
* @param array &$item Set to the current cycle's field-name => value data for this data source.
* @param int &$currenttime Set to the current cycle's sample timestamp for this data source.
*
* @return mixed The resolved current value, or an empty string when no usable sample exists.
*/
function thold_get_currentval(&$thold_data, &$rrd_reindexed, &$rrd_time_reindexed, &$item, &$currenttime) {
// adjust the polling interval by the last read, if applicable
$currenttime = $rrd_time_reindexed[$thold_data['local_data_id']];
$poller_interval = read_config_option('poller_interval');
if (!is_numeric($poller_interval) || $poller_interval <= 0) {
$poller_interval = 300;
}
if ($thold_data['lasttime'] > 0) {
if (is_numeric($currenttime)) {
$step = $currenttime - $thold_data['lasttime'];
} else {
$step = $thold_data['rrd_step'];
}
} else {
$step = $thold_data['rrd_step'];
}
$elapsed_step = $step;
if (!is_numeric($step) || $step <= 0) {
$step = $poller_interval;
}
$rrd_step = is_numeric($thold_data['rrd_step']) && $thold_data['rrd_step'] > 0
? (float) $thold_data['rrd_step']
: 0.0;
$sample_interval = max($rrd_step, (float) $poller_interval);
// Use a two-cycle floor so normal scheduler jitter does not discard the
// only usable pair even when an RRD heartbeat is tighter than poll cadence.
$rrd_heartbeat = is_numeric($thold_data['rrd_heartbeat'] ?? null) && $thold_data['rrd_heartbeat'] > 0
? max((float) $thold_data['rrd_heartbeat'], 2 * $sample_interval)
: 2 * $sample_interval;
$previous_sample_usable = $thold_data['lasttime'] > 0
&& is_numeric($elapsed_step)
&& $elapsed_step > 0
&& $elapsed_step <= $rrd_heartbeat;
if ($thold_data['lasttime'] > 0 && is_numeric($elapsed_step) && $elapsed_step > $rrd_heartbeat && function_exists('thold_debug')) {
thold_debug(sprintf(
'Threshold sample gap of %s seconds exceeds the effective heartbeat of %s seconds.',
$elapsed_step,
$rrd_heartbeat
), 'thold');
}
$currentval = '';
if (isset($rrd_reindexed[$thold_data['local_data_id']])) {
$item = $rrd_reindexed[$thold_data['local_data_id']];
if (isset($item[$thold_data['name']]) && is_numeric($item[$thold_data['name']])) {
switch ($thold_data['data_source_type_id']) {
case 2: // COUNTER
// A previous reading of zero is a real reading, not a missing one.
if ($previous_sample_usable && is_numeric($thold_data['oldvalue']) && $thold_data['oldvalue'] !== '') {
if ($item[$thold_data['name']] >= $thold_data['oldvalue']) {
// Everything is Normal
$currentval = $item[$thold_data['name']] - $thold_data['oldvalue'];
} else {
// Possible overflow, see if its 32bit or 64bit
$currentval = thold_counter_wrap_delta($thold_data['oldvalue'], $item[$thold_data['name']]);
}
if (strpos((string) $thold_data['rrd_maximum'], '|query_') !== false) {
$data_local = db_fetch_row_prepared('SELECT *
FROM data_local
WHERE id = ?',
[$thold_data['local_data_id']]);
if ($thold_data['rrd_maximum'] == '|query_ifSpeed|' || $thold_data['rrd_maximum'] == '|query_ifHighSpeed|') {
$highSpeed = db_fetch_cell_prepared("SELECT field_value
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND snmp_index = ?
AND field_name = 'ifHighSpeed'",
[$data_local['host_id'], $data_local['snmp_query_id'], $data_local['snmp_index']]);
if (!empty($highSpeed)) {
$thold_data['rrd_maximum'] = $highSpeed * 1000000;
} else {
$thold_data['rrd_maximum'] = substitute_snmp_query_data('|query_ifSpeed|', $data_local['host_id'], $data_local['snmp_query_id'], $data_local['snmp_index']);
}
} else {
$thold_data['rrd_maximum'] = substitute_snmp_query_data($thold_data['rrd_maximum'], $data_local['host_id'], $data_local['snmp_query_id'], $data_local['snmp_index']);
}
}
$maximum_value = $thold_data['rrd_maximum'] ?? '';
$rrd_maximum = is_numeric($maximum_value) ? (float) $maximum_value : 0.0;
// assume counter reset if greater than max value
if ($rrd_maximum > 0 && ($currentval / $step) > $rrd_maximum) {
$currentval = $item[$thold_data['name']] / $step;
} elseif ($rrd_maximum === 0.0 && $currentval > 4.25E+9 * max(1, $step / $sample_interval)) {
$currentval = $item[$thold_data['name']] / $step;
} else {
$currentval = $currentval / $step;
}
} else {
$currentval = '';
}
break;
case 3: // DERIVE
if ($previous_sample_usable && is_numeric($thold_data['oldvalue'])) {
$currentval = ($item[$thold_data['name']] - $thold_data['oldvalue']) / $step;
} else {
$currentval = '';
}
break;
case 4: // ABSOLUTE
$currentval = ($thold_data['lasttime'] <= 0 || $previous_sample_usable)
? $item[$thold_data['name']] / $step
: '';
break;
case 1: // GAUGE
default:
$currentval = $item[$thold_data['name']];
break;
}
}
}
return $currentval;
}
/**
* thold_calculate_expression - evaluates a threshold's RPN CDEF-style expression, resolving
* |ds:name| and |...| query-replacement tokens against the current poll's data, then reducing the
* resulting RPN token stream on an evaluation stack via the thold_expression_*_rpn() helpers.
*
* @param array $thold The threshold row, including its 'expression' string and local_data_id.
* @param mixed $currentval The threshold's own already-resolved current data source value.
* @param array &$rrd_reindexed local_data_id => (rrd field name => value) map for this poll cycle.
* @param array &$rrd_time_reindexed local_data_id => sample Unix timestamp map for this poll cycle.
*
* @global bool $rpn_error Set to true if any token/operand in the expression is invalid.
*
* @return mixed The reduced expression result, or an empty string when the expression could not
* be evaluated (unavailable source, unsupported token, or an unbalanced RPN stack).
*/
function thold_calculate_expression($thold, $currentval, &$rrd_reindexed, &$rrd_time_reindexed) {
global $rpn_error;
// set an rpn error flag
$rpn_error = false;
// operators to support
$math = ['+', '-', '*', '/', '%', '^', 'ADDNAN', 'SIN', 'COS', 'LOG', 'EXP',
'SQRT', 'ATAN', 'ATAN2', 'FLOOR', 'CEIL', 'DEG2RAD', 'RAD2DEG', 'ABS'];
$boolean = ['LT', 'LE', 'GT', 'GE', 'EQ', 'NE', 'UN', 'ISINF', 'IF', 'AND', 'OR'];
$comparison = ['MIN', 'MAX', 'LIMIT'];
$setops = ['SORT', 'REV', 'AVG'];
$specvals = ['UNKN', 'INF', 'NEGINF', 'PREV', 'COUNT'];
$stackops = ['DUP', 'POP', 'EXC'];
$time = ['NOW', 'TIME', 'LTIME'];
$spectypes = ['CURRENT_DATA_SOURCE', 'CURRENT_GRAPH_MINIMUM_VALUE',
'CURRENT_GRAPH_MAXIMUM_VALUE', 'CURRENT_DS_MINIMUM_VALUE',
'CURRENT_DS_MAXIMUM_VALUE', 'VALUE_OF_HDD_TOTAL',
'ALL_DATA_SOURCES_NODUPS', 'ALL_DATA_SOURCES_DUPS'];
// our expression array
$expression = explode(',', $thold['expression']);
// out current data sources. A threshold whose data source produced no
// readings this cycle has no entry here, so default rather than index blind.
$data_sources = $rrd_reindexed[$thold['local_data_id']] ?? [];
// replace all data tabs in the rpn with values
if (cacti_sizeof($expression)) {
foreach ($expression as $key => $item) {
if (strpos($item, '|ds:') !== false) {
// Remove invalid characters
$item = str_replace('\\', '', $item);
$dsname = trim(str_replace('|ds:', '', $item), " |\n\r");
$thold_item = db_fetch_row_prepared('SELECT td.id, td.local_graph_id,
td.percent_ds, td.expression, td.upper_ds, td.data_type,
td.host_id, td.cdef, td.local_data_id,
td.data_template_rrd_id, td.lastread, UNIX_TIMESTAMP(td.lasttime) AS lasttime,
td.oldvalue, dtr.data_source_name as name,
dtr.data_source_type_id, dtd.rrd_step, dtr.rrd_maximum, dtr.rrd_heartbeat
FROM thold_data AS td
LEFT JOIN data_template_rrd AS dtr
ON dtr.id = td.data_template_rrd_id
LEFT JOIN data_template_data AS dtd
ON dtd.local_data_id=td.local_data_id
WHERE dtr.data_source_name = ?
AND td.local_data_id = ?',
[$dsname, $thold['local_data_id']]);
if (!cacti_sizeof($thold_item)) {
$current_sample = $rrd_reindexed[$thold['local_data_id']][$dsname] ?? '';
if (is_numeric($current_sample)) {
$source = db_fetch_row_prepared('SELECT data_source_type_id
FROM data_template_rrd
WHERE local_data_id = ?
AND data_source_name = ?',
[$thold['local_data_id'], $dsname]);
if (cacti_sizeof($source) && $source['data_source_type_id'] == 1) {
$value = $current_sample;
} elseif (cacti_sizeof($source)) {
$value = '';
if (read_config_option('dsstats_enable') == 'on') {
$value = db_fetch_cell_prepared('SELECT calculated
FROM data_source_stats_hourly_last
WHERE local_data_id = ?
AND rrd_name = ?',
[$thold['local_data_id'], $dsname]);
}
if (!is_numeric($value) || $value == -90909090909) {
$value = get_current_value($thold['local_data_id'], $dsname, 0, '');
}
} else {
$value = '';
}
if (is_numeric($value)) {
$expression[$key] = $value;
continue;
}
}
if (is_numeric($thold['lastread'] ?? null)) {
cacti_log(sprintf(
'WARNING: Threshold %s expression source %s is unavailable for local data ID %s.',
$thold['id'] ?? 'unknown',
$dsname,
$thold['local_data_id'] ?? 'unknown'
), false, 'THOLD', POLLER_VERBOSITY_MEDIUM);
}
return '';
}
$item = [];
$currenttime = 0;
$value = thold_get_currentval($thold_item, $rrd_reindexed, $rrd_time_reindexed, $item, $currenttime);
if (!is_numeric($value)) {
return '';
}
$expression[$key] = $value;
} elseif (strpos($item, '|') !== false) {
// Remove invalid characters
$item = str_replace('\\', '', $item);
$gl = db_fetch_row_prepared('SELECT *
FROM graph_local
WHERE id = ?',
[$thold['local_graph_id']]);
if (cacti_sizeof($gl)) {
$expression[$key] = thold_expand_string($thold, $item);
} else {
$expression[$key] = '0';
cacti_log("WARNING: Query Replacement for '$item' Does Not Exist");
}
} else {
// normal operator
}
}
}
$processed_expression = $expression;
// cacti_log(implode(',', array_keys($data_sources)));
// cacti_log(implode(',', $data_sources));
// cacti_log(implode(',', $expression));
// now let's process the RPN stack
$x = count($expression);
if ($x == 0) {
return $currentval;
}
// operation stack for RPN
$stack = [];
// current pointer in the RPN operations list
$cursor = 0;
while ($cursor < $x) {
$operator = strtoupper(trim($expression[$cursor]));
if (stripos($operator, '|query_ifHighSpeed') !== false || stripos($operator, '|query_ifSpeed') !== false) {
$data_local = db_fetch_row_prepared('SELECT *
FROM data_local
WHERE id = ?',
[$thold['local_data_id']]);
$operator = rrdtool_function_interface_speed($data_local);
}
// is the operator a data source
if (is_numeric($operator)) {
// cacti_log("NOTE: Numeric '$operator'", false, "THOLD");
array_push($stack, $operator);
} elseif (array_key_exists($operator, $data_sources)) {
// cacti_log("NOTE: DS Value '$operator'", false, "THOLD");
thold_expression_ds_value($operator, $stack, $data_sources);
} elseif (in_array($operator, $comparison, true)) {
// cacti_log("NOTE: Compare '$operator'", false, "THOLD");
thold_expression_compare_rpn($operator, $stack);
} elseif (in_array($operator, $boolean, true)) {
// cacti_log("NOTE: Boolean '$operator'", false, "THOLD");
thold_expression_boolean_rpn($operator, $stack);
} elseif (in_array($operator, $math, true)) {
// cacti_log("NOTE: Math '$operator'", false, "THOLD");
thold_expression_math_rpn($operator, $stack);
} elseif (in_array($operator, $setops, true)) {
// cacti_log("NOTE: SetOps '$operator'", false, "THOLD");
thold_expression_setops_rpn($operator, $stack);
} elseif (in_array($operator, $specvals, true)) {
// cacti_log("NOTE: SpecVals '$operator'", false, "THOLD");
thold_expression_specvals_rpn($operator, $stack, $cursor + 2);
} elseif (in_array($operator, $stackops, true)) {
// cacti_log("NOTE: StackOps '$operator'", false, "THOLD");
thold_expression_stackops_rpn($operator, $stack);
} elseif (in_array($operator, $time, true)) {
// cacti_log("NOTE: Time '$operator'", false, "THOLD");
thold_expression_time_rpn($operator, $stack);
} elseif (in_array($operator, $spectypes, true)) {
// cacti_log("NOTE: SpecialTypes '$operator'", false, "THOLD");
thold_expression_specialtype_rpn($operator, $stack, $thold['local_data_id'], $currentval);
} else {
cacti_log("WARNING: Unsupported Field '$operator'", false, 'THOLD');
$rpn_error = true;
}
$cursor++;
if ($rpn_error) {
cacti_log("ERROR: RPN Expression is invalid! THold:'" . $thold['name'] . "', Value:'" . $currentval . "', Expression:'" . $thold['expression'] . "', Processed:'" . implode(',', $processed_expression) . "'", false, 'THOLD');
// Fail closed: '' is this function's established unavailable-sample
// sentinel (see the early returns above). Returning a numeric 0 here
// would pass is_numeric() in the polling fail-closed guard and be
// persisted/alerted on as a real, valid zero reading.
return '';
}
}
// rrdtool yields the top of the stack. Anything left below it means the
// expression was unbalanced, which is an authoring error worth reporting.
if (cacti_sizeof($stack) != 1) {
cacti_log("ERROR: RPN Expression did not reduce to a single value! THold:'" . $thold['name'] . "', Expression:'" . $thold['expression'] . "', Stack:'" . implode(',', $stack) . "'", false, 'THOLD');
$rpn_error = true;
return '';
}
return end($stack);
}
/**
* thold_substitute_snmp_query_data - resolves a `|query_<field>|`-style token to the matching
* cached SNMP data-query field value for a device/index, if present.
*
* @param string $string The raw `|query_<field>|` token to resolve.
* @param int $device_id The device (host) id to look up the cached value for.
* @param int $snmp_query_id The SNMP data query id the field belongs to.
* @param string $snmp_index The data query's index value for the target row.
* @param int $max_chars Unused; retained for call-site compatibility with sibling substitute
* helpers.
*
* @return string The resolved field value, or the original token when no cached value exists.
*/
function thold_substitute_snmp_query_data($string, $device_id, $snmp_query_id, $snmp_index, $max_chars = 0) {
$field_name = trim(str_replace('|query_', '', $string),"| \n\r");
$snmp_cache_data = db_fetch_cell_prepared('SELECT field_value
FROM host_snmp_cache
WHERE host_id = ?
AND snmp_query_id = ?
AND snmp_index = ?
AND field_name= ?',
[$device_id, $snmp_query_id, $snmp_index, $field_name]);
if ($snmp_cache_data != '') {
return $snmp_cache_data;
} else {
return $string;
}
}
/**
* thold_substitute_data_source_description - resolves a `|data_source_description|` token to the
* data source's cached display name, if present.
*
* @param string $string The raw `|data_source_description|` token to resolve.
* @param int $local_data_id The local data source id to look up the cached name for.
* @param int $max_chars Unused; retained for call-site compatibility with sibling substitute
* helpers.
*
* @return string The resolved description, or the original token when no cached value exists.
*/
function thold_substitute_data_source_description($string, $local_data_id, $max_chars = 0) {
$field_name = trim(str_replace('|data_source_description', '', $string),"| \n\r");
$cache_data = db_fetch_cell_prepared('SELECT name_cache
FROM data_template_data
WHERE local_data_id = ?
LIMIT 1',
[$local_data_id]);
if ($cache_data != '') {
return $cache_data;
} else {
return $string;
}
}
/**
* A token that cannot appear in any admin-authored template and cannot match
* any of this file's own token patterns (<TAG>, |pipe|), so a later
* substitution phase can never mistake it for real token syntax.
*
* Used to defer inserting an already-quoted, device/operator-controlled
* value until every phase that scans for token syntax has run; only then is
* it safe to reveal the value without a later phase re-scanning it and
* breaking out of its quoting.
*
* @return string
*/
function thold_defer_placeholder() {
static $n = 0;
$n++;
return "\x01THOLD_DEFERRED_{$n}\x01";
}
/**
* thold_substitute_host_data - resolves `|host_<field>|` tokens (and the special
* `|host_management_ip|` alias) in a string against the target device's cached host row.
*
* @param string $string The raw string containing `|host_*|` tokens to resolve.
* @param string $l_escape_string The left delimiter used around each token.
* @param string $r_escape_string The right delimiter used around each token.
* @param int $device_id The device (host) id to resolve field values against.
* @param bool $shell When true, substituted values are shell-escaped and deferred via
* thold_defer_placeholder() until every substitution phase has run.
* @param array|null &$deferred Shared placeholder => shell-escaped-value map; pass null to have
* this call resolve its own placeholders before returning.
*
* @return string The string with all resolvable `|host_*|` tokens substituted.
*/
function thold_substitute_host_data($string, $l_escape_string, $r_escape_string, $device_id, $shell = false, ?array &$deferred = null) {
$defer_internally = $deferred === null;
if ($defer_internally) {
$deferred = [];
}
$field_name = trim(str_replace('|host_', '', $string),"| \n\r");
if (!isset($_SESSION['sess_host_cache_array'][$device_id])) {
$host = db_fetch_row_prepared('SELECT *
FROM host WHERE id = ?',
[$device_id]);
$_SESSION['sess_host_cache_array'][$device_id] = $host;
}
if (isset($_SESSION['sess_host_cache_array'][$device_id][$field_name])) {
$field_value = $_SESSION['sess_host_cache_array'][$device_id][$field_name];
if (!$shell) {
return $field_value;
}
$placeholder = thold_defer_placeholder();
$deferred[$placeholder] = cacti_escapeshellarg((string) $field_value);
return $defer_internally ? $deferred[$placeholder] : $placeholder;
}
$hostname = $_SESSION['sess_host_cache_array'][$device_id]['hostname'];
if ($shell) {
$placeholder = thold_defer_placeholder();
$deferred[$placeholder] = cacti_escapeshellarg((string) $hostname);
$string = str_replace($l_escape_string . 'host_management_ip' . $r_escape_string, $placeholder, $string);
} else {
$string = str_replace($l_escape_string . 'host_management_ip' . $r_escape_string, $hostname, $string);
}
$temp = api_plugin_hook_function('substitute_host_data', ['string' => $string, 'l_escape_string' => $l_escape_string, 'r_escape_string' => $r_escape_string, 'host_id' => $device_id]);
$string = $temp['string'];
if ($defer_internally) {
$string = strtr($string, $deferred);
}
return $string;
}
/**
* thold_substitute_custom_data - takes a string and substitutes all custom data variables contained in it
*
* @param $string - the string to make custom data variable substitutions on
* @param $l_escape_string - the character used to escape each variable on the left side
* @param $r_escape_string - the character used to escape each variable on the right side
* @param $local_data_id - (int) the local_data_id to match
* @param mixed $l_escape
* @param mixed $r_escape
*
* @return - the original string with all of the variable substitutions made
*/
function thold_substitute_custom_data($string, $l_escape, $r_escape, $local_data_id, $shell = false, ?array &$deferred = null) {
$defer_internally = $deferred === null;
if ($defer_internally) {
$deferred = [];
}
if (is_array($local_data_id)) {
$local_data_ids = $local_data_id;
} elseif ($local_data_id == '') {
return;
} else {
$local_data_ids = [$local_data_id];
}
$match = $l_escape . 'custom_';
$matches = [];
$inclause = [];
$query_array = [];
$cur_pos = 0;
while (true) {
$start_pos = strpos($string, $match, $cur_pos);
if ($start_pos !== false) {
$cur_pos = strpos($string, $r_escape, $start_pos + 1);
$cfield = substr($string, $start_pos, $cur_pos - $start_pos + 1);
$cfield = trim(str_replace($match, '', $cfield), " \n\r\t$r_escape");
$matches[] = $cfield;
$inclause[] = '?';
} else {
break;
}
}
if (cacti_sizeof($matches)) {
$ids = array_rekey(
db_fetch_assoc('SELECT id
FROM data_template_data
WHERE local_data_id IN (' . implode(', ', $local_data_ids) . ')'),
'id', 'id'
);
if (cacti_sizeof($ids)) {
$data_template_data_ids = implode(', ', $ids);
foreach ($matches as $match) {
$query_array[] = $match;
}
$custom_data_array = db_fetch_assoc_prepared('SELECT
dif.data_name AS name, did.value
FROM data_input_fields AS dif
INNER JOIN data_input_data AS did
ON did.data_input_field_id = dif.id
WHERE did.data_template_data_id IN (' . $data_template_data_ids . ')
AND data_name IN (' . implode(', ', $inclause) . ')',
$query_array);
if (cacti_sizeof($custom_data_array)) {
$replacements = [];
foreach ($custom_data_array as $custom_data) {
$custom_name = $custom_data['name'];
$token = $l_escape . 'custom_' . $custom_name . $r_escape;
if ($shell) {
$placeholder = thold_defer_placeholder();
$deferred[$placeholder] = cacti_escapeshellarg((string) $custom_data['value']);
$replacements[$token] = $placeholder;
} else {
$replacements[$token] = $custom_data['value'];
}
}
// A single strtr() pass replaces against the original string only and
// never re-scans inserted values, so one custom field's value cannot
// contain another field's literal token and have it substituted again
// outside of its own quoting.
$string = strtr($string, $replacements);
}
}
}
if ($defer_internally) {
$string = strtr($string, $deferred);
}
return $string;
}
/**
* thold_calculate_percent - converts a threshold's current value into a percentage of a second,
* configured 'percent_ds' data source's current value in the same poll cycle.
*
* @param array $thold The threshold row, including the 'percent_ds' field name.
* @param mixed $currentval The threshold's already-resolved current value.
* @param array $rrd_reindexed local_data_id => (rrd field name => value) map for this poll cycle.
*
* @return mixed The percentage value, or an empty string when either value is unavailable.
*/
function thold_calculate_percent($thold, $currentval, $rrd_reindexed) {
$ds = $thold['percent_ds'];
if (!is_numeric($currentval)) {
return '';
}
if (isset($rrd_reindexed[$thold['local_data_id']][$ds])) {
// Not cast to int: a denominator below one truncated to zero, which
// forced the percentage to zero and kept a low threshold alerting.
$t = $rrd_reindexed[$thold['local_data_id']][$thold['percent_ds']];
if (!is_numeric($t)) {
$currentval = '';
} elseif ($t != 0) {
$currentval = ($currentval / $t) * 100;
} else {
$currentval = 0;
}
} else {
$currentval = '';
}
return $currentval;
}
/**
* thold_calculate_lower_upper - combines a threshold's current value with a second, configured
* 'upper_ds' data source's current value into one 64-bit-scale composite reading (used for
* upper/lower 32-bit counter pairs that together represent a single wider counter).
*
* @param array $thold The threshold row, including the 'upper_ds' field name.
* @param mixed $currentval The threshold's already-resolved current (lower) value.
* @param array $rrd_reindexed local_data_id => (rrd field name => value) map for this poll cycle.
*
* @return mixed The combined value, or an empty string when either value is unavailable/invalid.
*/
function thold_calculate_lower_upper($thold, $currentval, $rrd_reindexed) {
$ds = $thold['upper_ds'];
if (!is_numeric($currentval)) {
return '';
}
if (!isset($rrd_reindexed[$thold['local_data_id']][$ds])) {
return '';
}
$t = $rrd_reindexed[$thold['local_data_id']][$thold['upper_ds']];
if (!is_numeric($t) || $t < 0 || $t > 4294967295) {
return '';
}
return ((float) $t * 4294967296) + $currentval;
}
/**
* get_allowed_thresholds - fetches the thresholds a user is permitted to see, applying graph
* authentication policy filtering and optional caller-supplied WHERE/ORDER BY/LIMIT clauses.
*
* @param string $sql_where Additional SQL WHERE conditions (without the WHERE keyword).
* @param string $order_by SQL ORDER BY expression (without the ORDER BY keyword).
* @param string $sql_limit SQL LIMIT clause contents (without the LIMIT keyword).
* @param int &$total_rows Set to the total matching row count (ignoring $sql_limit).
* @param int $user_id The user id to check permissions for; 0 uses the session user, -1 skips
* authentication entirely.
* @param int $graph_id Optional graph id to restrict results to a single graph.
* @param array $sql_params Bound parameters for any placeholders already present in $sql_where.
*
* @return array The matching thold_data rows, joined with template/graph/host/data-source info.
*/
function get_allowed_thresholds($sql_where = '', $order_by = 'td.name', $sql_limit = '', &$total_rows = 0, $user_id = 0, $graph_id = 0, $sql_params = []) {
$graph_id = (int) $graph_id;
if ($sql_limit != '') {
$sql_limit = "LIMIT $sql_limit";
}
if ($order_by != '') {
$order_by = "ORDER BY $order_by";
}
$params = $sql_params;
if ($graph_id > 0) {
$sql_where .= (strlen($sql_where) ? ' AND ' : ' ') . ' gl.id = ?';
$params[] = $graph_id;
}
if (strlen($sql_where)) {
$sql_where = "WHERE $sql_where";
}
if ($user_id == -1) {
$auth_method = 0;
} else {
$auth_method = read_config_option('auth_method');
}
if ($auth_method > 0 && $user_id == 0) {
if (isset($_SESSION['sess_user_id'])) {
$user_id = $_SESSION['sess_user_id'];
} else {
return [];
}
}
// see if permissions are simple
$simple_perms = get_simple_graph_perms($user_id);
// in case we need to review get the graph_auth_method
$graph_auth_method = read_config_option('graph_auth_method');
// get policies for all groups and user
$policies = get_policies($user_id);
if (!$simple_perms && $auth_method != 0) {
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
}
$tholds_sql = ("SELECT
td.*, dtd.rrd_step, tt.name AS template_name, dtr.data_source_name AS data_source,
IF(IFNULL(td.`lastread`,'') = '',NULL,(td.`lastread` + 0.0)) AS `flastread`,
IF(IFNULL(td.`oldvalue`,'') = '',NULL,(td.`oldvalue` + 0.0)) AS `foldvalue`,
UNIX_TIMESTAMP() - UNIX_TIMESTAMP(lastchanged) AS `instate`
FROM thold_data AS td
INNER JOIN graph_local AS gl
ON gl.id = td.local_graph_id
LEFT JOIN graph_templates AS gt
ON gt.id = gl.graph_template_id
LEFT JOIN host AS h
ON h.id = gl.host_id
LEFT JOIN thold_template AS tt
ON tt.id = td.thold_template_id
LEFT JOIN data_template_data AS dtd
ON dtd.local_data_id = td.local_data_id
LEFT JOIN data_template_rrd AS dtr
ON dtr.id = td.data_template_rrd_id
$sql_where
$order_by
$sql_limit");
$tholds = db_fetch_assoc_prepared($tholds_sql, $params);
$sql = "SELECT COUNT(*)
FROM (
SELECT td.id
FROM thold_data AS td
INNER JOIN graph_local AS gl
ON gl.id=td.local_graph_id
LEFT JOIN graph_templates AS gt
ON gt.id=gl.graph_template_id
LEFT JOIN host AS h
ON h.id=gl.host_id
LEFT JOIN thold_template AS tt
ON tt.id=td.thold_template_id
$sql_where
) AS rower";
if (function_exists('get_total_row_data') && $graph_id == 0) {
$total_rows = get_total_row_data($user_id, $sql, $params, 'thold', 10);
} else {
$total_rows = db_fetch_cell_prepared($sql, $params);
}
return $tholds;
}
/**
* get_allowed_threshold_logs - fetches the plugin_thold_log entries a user is permitted to see,
* applying graph authentication policy filtering and optional caller-supplied WHERE/ORDER
* BY/LIMIT clauses.
*
* @param string $sql_where Additional SQL WHERE conditions (without the WHERE keyword).
* @param string $order_by SQL ORDER BY expression (without the ORDER BY keyword).
* @param string $sql_limit SQL LIMIT clause contents (without the LIMIT keyword).
* @param int &$total_rows Set to the total matching row count (ignoring $sql_limit).
* @param int $user_id The user id to check permissions for; 0 uses the session user, -1 skips
* authentication entirely.
* @param int $graph_id Optional graph id to restrict results to a single graph.
* @param array $sql_params Bound parameters for any placeholders already present in $sql_where.
*
* @return array The matching plugin_thold_log rows, joined with threshold/graph/host info.
*/
function get_allowed_threshold_logs($sql_where = '', $order_by = 'td.name', $sql_limit = '', &$total_rows = 0, $user_id = 0, $graph_id = 0, $sql_params = []) {
$graph_id = (int) $graph_id;
if ($sql_limit != '') {
$sql_limit = "LIMIT $sql_limit";
}
if ($order_by != '') {
$order_by = "ORDER BY $order_by";
}
$params = $sql_params;
if ($graph_id > 0) {
$sql_where .= (strlen($sql_where) ? ' AND ' : ' ') . ' gl.id = ?';
$params[] = $graph_id;
}
if (strlen($sql_where)) {
$sql_where = "WHERE $sql_where";
}
if ($user_id == -1) {
$auth_method = 0;
} else {
$auth_method = read_config_option('auth_method');
}
if ($auth_method > 0 && $user_id == 0) {
if (isset($_SESSION['sess_user_id'])) {
$user_id = $_SESSION['sess_user_id'];
} else {
return [];
}
}
// see if permissions are simple
$simple_perms = get_simple_graph_perms($user_id);
// in case we need to review get the graph_auth_method
$graph_auth_method = read_config_option('graph_auth_method');
// get policies for all groups and user
$policies = get_policies($user_id);
if (!$simple_perms && $auth_method != 0) {
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
}
$tholds = db_fetch_assoc_prepared("SELECT
tl.`id`, tl.`time`, tl.`host_id`, tl.`local_graph_id`, tl.`threshold_id`,
IF(IFNULL(tl.`threshold_value`,'')='',NULL,(tl.`threshold_value` + 0.0)) AS `threshold_value`,
IF(IFNULL(tl.`current`,'')='',NULL,(tl.`current` + 0.0)) AS `current`, tl.`status`, tl.`type`,
tl.`description`, h.description AS hdescription, td.name, gtg.title_cache
FROM plugin_thold_log AS tl
INNER JOIN thold_data AS td
ON tl.threshold_id=td.id
INNER JOIN graph_local AS gl
ON gl.id=td.local_graph_id
LEFT JOIN graph_templates AS gt
ON gt.id=gl.graph_template_id
LEFT JOIN graph_templates_graph AS gtg
ON gtg.local_graph_id=gl.id
LEFT JOIN host AS h
ON h.id=gl.host_id
$sql_where
$order_by
$sql_limit", $params);
$sql = "SELECT COUNT(*)
FROM (
SELECT tl.id
FROM plugin_thold_log AS tl
INNER JOIN thold_data AS td
ON tl.threshold_id=td.id
INNER JOIN graph_local AS gl
ON gl.id=td.local_graph_id
LEFT JOIN graph_templates AS gt
ON gt.id=gl.graph_template_id
LEFT JOIN graph_templates_graph AS gtg
ON gtg.local_graph_id=gl.id
LEFT JOIN host AS h
ON h.id=gl.host_id
$sql_where
) AS rower";
if (function_exists('get_total_row_data') && $graph_id == 0) {
$total_rows = get_total_row_data($user_id, $sql, $params, 'thold_log', 10);
} else {
$total_rows = db_fetch_cell_prepared($sql, $params);
}
return $tholds;
}
/**
* thold_get_thold_notification_format_file - resolves the notification format file to use for a
* threshold, preferring the notification list's own format file over the threshold's own.
*
* @param int $thold_id The threshold id to look up a fallback format file for.
* @param int $notify_list The notification list id to prefer a format file from, if set.
*
* @return string The resolved format file name, or an empty string when neither is set.
*/
function thold_get_thold_notification_format_file($thold_id, $notify_list) {
$thold_format_file = db_fetch_cell_prepared('SELECT format_file
FROM thold_data
WHERE id = ?',
[$thold_id]);
$notify_format_file = '';
if ($notify_list > 0) {
$notify_format_file = db_fetch_cell_prepared('SELECT format_file
FROM plugin_notification_lists
WHERE id = ?',
[$notify_list]);
}
if ($notify_format_file != '') {
return $notify_format_file;
}
if ($thold_format_file != '') {
return $thold_format_file;
} else {
return '';
}
}
/**
* thold_get_device_notification_format_file - resolves the notification format file to use for a
* device-status event, preferring the notification list's own format file.
*
* @param int $host_id Unused directly (reserved for a future per-device format file); kept for
* call-site symmetry with thold_get_thold_notification_format_file().
* @param int $notify_list The notification list id to look up a format file from, if set.
*
* @return string The resolved format file name, or an empty string when none is set.
*/
function thold_get_device_notification_format_file($host_id, $notify_list) {
$device_format_file = '';
if ($notify_list > 0) {
$notify_format_file = db_fetch_cell_prepared('SELECT format_file
FROM plugin_notify_list
WHERE id = ?',
[$notify_list]);
}
if ($notify_format_file != '') {
return $notify_format_file;
}
if ($device_format_file != '') {
return $device_format_file;
} else {
return '';
}
}
/**
* thold_get_state_filter - builds a SQL WHERE-clause fragment for the threshold list's 'state'
* request-variable filter (All/Breached/Triggered/Enabled/Disabled/Ack Required, etc.).
*
* @param string $state Non-empty to apply the currently-selected 'state' request variable.
*
* @return string The SQL fragment for the selected state, or an empty string for 'All'/unset.
*/
function thold_get_state_filter($state) {
$statefilter = '';
if ($state != '') {
if (get_request_var('state') == '-1') { // All
$statefilter = '';
} elseif (get_request_var('state') == '0') { // Breached or Triggered
$statefilter = '((td.thold_enabled = "on" AND td.thold_per_enabled = "on") AND
(td.thold_alert != 0 OR td.bl_alert > 0 OR ((td.thold_alert != 0 AND td.thold_fail_count >= td.thold_fail_trigger) OR (td.bl_alert > 0 AND td.bl_fail_count >= td.bl_fail_trigger))))';
} elseif (get_request_var('state') == '7') { // Disabled at Template
$statefilter = '(td.thold_enabled = "off")';
} elseif (get_request_var('state') == '2') { // Enabled
$statefilter = '(td.thold_enabled = "on" AND td.thold_per_enabled = "on")';
} elseif (get_request_var('state') == '1') { // Breached
$statefilter = '((td.thold_enabled = "on" AND td.thold_per_enabled = "on") AND (td.thold_alert != 0 OR td.bl_alert > 0))';
} elseif (get_request_var('state') == '3') { // Triggered
$statefilter = '((td.thold_enabled = "on" AND td.thold_per_enabled = "on") AND ((td.thold_alert != 0 AND td.thold_fail_count >= td.thold_fail_trigger) OR (td.bl_alert > 0 AND td.bl_fail_count >= td.bl_fail_trigger)))';
} elseif (get_request_var('state') == '4') { // Ack Required
$statefilter = '(td.acknowledgment = "on")';
} elseif (get_request_var('state') == '5') { // Disabled at Thold
$statefilter = '(td.thold_per_enabled = "")';
} elseif (get_request_var('state') == '6') { // Disabled all together
$statefilter = '(td.thold_per_enabled = "" OR td.thold_enabled = "off")';
} elseif (get_request_var('state') >= 101 && get_request_var('state') <= 107) { // Severity (status pill) 101=Alert .. 107=Disabled
$statefilter = '(' . thold_severity_sql() . ' = ' . ((int) get_request_var('state') - 100) . ')';
}
}
return $statefilter;
}
/**
* is_thold_allowed_graph - checks whether the current user is permitted to view the graph a
* threshold belongs to.
*
* @param int $local_graph_id The local graph id to check.
*
* @return bool True if the current user is permitted to view the graph.
*/
function is_thold_allowed_graph($local_graph_id) {
return is_graph_allowed($local_graph_id);
}
/**
* is_thold_allowed - checks whether the current user is permitted to view the graph a threshold
* belongs to, looking the graph up from the threshold id.
*
* @param int $id The threshold id to check.
*
* @return bool True if the current user is permitted to view the threshold's graph.
*/
function is_thold_allowed($id) {
$local_graph_id = db_fetch_cell_prepared('SELECT local_graph_id
FROM thold_data
WHERE id = ?',
[$id]);
return is_thold_allowed_graph($local_graph_id);
}
/**
* thold_log - records a threshold breach/restoral (or acknowledgment/custom description) event
* into plugin_thold_log, optionally also writing a formatted summary line to the Cacti log, and
* refreshes the thold change-tracking timestamps.
*
* @param array $save The plugin_thold_log row to save (threshold_id/type/status/current/etc.,
* plus optional 'emails'/'bcc_emails' for the Cacti-log summary line only).
*
* @global array $config Cacti's global configuration array.
*
* @return void
*/
function thold_log($save) {
global $config;
include($config['base_path'] . '/plugins/thold/includes/arrays.php');
if ($save['current'] == null) {
$save['current'] = '';
}
$save['id'] = 0;
if (read_config_option('thold_log_cacti') == 'on') {
$thold = db_fetch_row_prepared('SELECT *
FROM thold_data
WHERE id = ?',
[$save['threshold_id']]);
$dt = db_fetch_cell_prepared('SELECT data_template_id
FROM data_template_data
WHERE local_data_id = ?',
[$thold['local_data_id']]);
$tname = db_fetch_cell_prepared('SELECT name
FROM data_template
WHERE id = ?',
[$dt]);
if ($save['type'] != 99) {
if ($save['status'] == 0) {
$desc = 'Threshold Restored ID: ' . $save['threshold_id'];
} else {
$desc = 'Threshold Breached ID: ' . $save['threshold_id'];
}
$desc .= ' DataTemplate: ' . $tname;
$desc .= ' DataSource: ' . $thold['data_source_name'];
if (isset($thold_types[$thold['thold_type']])) {
$desc .= ' Type: ' . $thold_types[$thold['thold_type']];
}
$desc .= ' Enabled: ' . ($thold['thold_enabled'] == 'on' && $thold['thold_per_enabled'] == 'on' ? 'on' : 'off');
switch ($thold['thold_type']) {
case 0:
$desc .= ' Current: ' . $save['current'];
$desc .= ' High: ' . $thold['thold_hi'];
$desc .= ' Low: ' . $thold['thold_low'];
$desc .= ' Trigger: ' . plugin_thold_duration_convert($thold['local_data_id'], $thold['thold_fail_trigger'], 'alert');
$desc .= ' Warning High: ' . $thold['thold_warning_hi'];
$desc .= ' Warning Low: ' . $thold['thold_warning_low'];
$desc .= ' Warning Trigger: ' . plugin_thold_duration_convert($thold['local_data_id'], $thold['thold_warning_fail_trigger'], 'alert');
break;
case 1:
$desc .= ' Current: ' . $save['current'];
break;
case 2:
$desc .= ' Current: ' . $save['current'];
$desc .= ' High: ' . $thold['time_hi'];
$desc .= ' Low: ' . $thold['time_low'];
$desc .= ' Trigger: ' . $thold['time_fail_trigger'];
$desc .= ' Time: ' . plugin_thold_duration_convert($thold['local_data_id'], $thold['time_fail_length'], 'time');
$desc .= ' Warning High: ' . $thold['time_warning_hi'];
$desc .= ' Warning Low: ' . $thold['time_warning_low'];
$desc .= ' Warning Trigger: ' . $thold['time_warning_fail_trigger'];
$desc .= ' Warning Time: ' . plugin_thold_duration_convert($thold['local_data_id'], $thold['time_warning_fail_length'], 'time');
break;
}
$desc .= ' SentTo: ' . (isset($save['emails']) && $save['emails'] != '' ? $save['emails'] : 'None');
$desc .= ' SendBCC: ' . (isset($save['bcc_emails']) && $save['bcc_emails'] != '' ? $save['bcc_emails'] : 'None');
} elseif (isset($save['description'])) {
$desc = $save['description'];
} else {
$desc = 'Threshold Acknowledgment';
}
if ($save['status'] == ST_RESTORAL || $save['status'] == ST_NOTIFYRS) {
thold_cacti_log($desc);
}
}
unset($save['emails']);
unset($save['bcc_emails']);
$id = sql_save($save, 'plugin_thold_log');
set_config_option('time_last_change_thold_log', time());
set_config_option('time_last_change_thold', time());
}
/**
* plugin_thold_duration_convert - converts a stored repeat/alert/time duration code into its
* poller-interval-relative display label, using the data source's own rrd_step where applicable.
*
* @param int $rra The local_data_id (or, per $field, the referenced id) whose rrd_step to use.
* @param mixed $data The raw stored duration code to convert.
* @param string $type Which lookup array to use: 'repeat', 'alert', or 'time'.
* @param string $field The column name to match $rra against (default 'local_data_id').
*
* @global array $config Cacti's global configuration array.
* @global array $repeatarray Repeat-duration code => display label map (loaded from arrays.php).
* @global array $alertarray Alert-duration code => display label map (loaded from arrays.php).
* @global array $timearray Time-duration code => display label map (loaded from arrays.php).
*
* @return mixed The display label for the code, or the raw $data when unmapped or $type is
* unrecognized, or an empty string when $data is empty.
*/
function plugin_thold_duration_convert($rra, $data, $type, $field = 'local_data_id') {
global $config, $repeatarray, $alertarray, $timearray;
// handle a null data value
if ($data == '') {
return '';
}
$step = db_fetch_cell_prepared("SELECT rrd_step
FROM data_template_data
WHERE $field = ?",
[$rra]);
include($config['base_path'] . '/plugins/thold/includes/arrays.php');
switch ($type) {
case 'repeat':
return (isset($repeatarray[$data]) ? $repeatarray[$data] : $data);
break;
case 'alert':
return (isset($alertarray[$data]) ? $alertarray[$data] : $data);
break;
case 'time':
return (isset($timearray[$data]) ? $timearray[$data] : $data);
break;
}
return $data;
}
/**
* plugin_thold_log_changes - writes a human-readable audit-log line to the Cacti log describing
* an administrative change (acknowledge/enable/disable a threshold or device, reapply a
* suggested name, template auto-creation, etc.), when the 'thold_log_changes' setting is enabled.
*
* @param int $id The affected threshold or device id (meaning depends on $changed).
* @param string $changed The change type being logged (e.g. 'acknowledge_threshold',
* 'enabled_host', 'auto_created').
* @param array $message Additional context used by some change types to enrich the log line.
*
* @global array $config Cacti's global configuration array.
*
* @return void
*/
function plugin_thold_log_changes($id, $changed, $message = []) {
global $config;
include($config['base_path'] . '/plugins/thold/includes/arrays.php');
$desc = '';
if (read_config_option('thold_log_changes') != 'on') {
return;
}
if (!$config['is_web']) {
$user = 'poller';
} elseif (isset($_SESSION['sess_user_id'])) {
$user = $_SESSION['sess_user_id'];
} else {
$user = 'unknown';
}
switch ($changed) {
case 'acknowledge_threshold':
$thold = db_fetch_row_prepared('SELECT *
FROM thold_data
WHERE id = ?',
[$id]);
$tname = db_fetch_cell_prepared('SELECT name
FROM data_template
WHERE id = ?',
[$thold['data_template_id']]);
$desc = "Threshold Acknowledged by User[$user] TH[$id]";
$desc .= ' DataTemplate[' . $tname . ']';
$desc .= ' DataSource[' . $thold['data_source_name'] . ']';
break;
case 'enabled_threshold':
$thold = db_fetch_row_prepared('SELECT *
FROM thold_data
WHERE id = ?',
[$id]);
$tname = db_fetch_cell_prepared('SELECT name
FROM data_template
WHERE id = ?',
[$thold['data_template_id']]);
$desc = "Threshold Enabled by User[$user] TH[$id]";
$desc .= ' DataTemplate[' . $tname . ']';
$desc .= ' DataSource[' . $thold['data_source_name'] . ']';
break;
case 'disabled_threshold':
$thold = db_fetch_row_prepared('SELECT *
FROM thold_data
WHERE id = ?',
[$id]);
$tname = db_fetch_cell_prepared('SELECT name
FROM data_template
WHERE id = ?',
[$thold['data_template_id']]);
$desc = "Threshold Disabled by User[$user] TH[$id]";
$desc .= ' DataTemplate[' . $tname . ']';
$desc .= ' DataSource[' . $thold['data_source_name'] . ']';
break;
case 'reapply_name':
$thold = db_fetch_row_prepared('SELECT *
FROM thold_data
WHERE id = ?',
[$id]);
$tname = db_fetch_cell_prepared('SELECT name
FROM data_template
WHERE id = ?',
[$thold['data_template_id']]);
$desc = "Threshold Reapply Suggested Name by User[$user] TH[$id]";
$desc .= ' DataTemplate[' . $tname . ']';
$desc .= ' DataSource[' . $thold['data_source_name'] . ']';
break;
case 'enabled_host':
$host = db_fetch_row_prepared('SELECT *
FROM host
WHERE id = ?',
[$id]);
$desc = "Device Enabled by User[$user] Device[$id]";
break;
case 'disabled_host':
$host = db_fetch_row_prepared('SELECT *
FROM host
WHERE id = ?',
[$id]);
$desc = "Device Disabled by User[$user] Device[$id]";
break;
case 'auto_created':
$thold = db_fetch_row_prepared('SELECT *
FROM thold_data
WHERE id = ?',
[$id]);
$tname = db_fetch_cell_prepared('SELECT name
FROM data_template
WHERE id = ?',
[$thold['data_template_id']]);
$desc = "Threshold Auto-created by User[$user] TH[$id]";
$desc .= ' DataTemplate[' . $tname . ']';
$desc .= ' DataSource[' . $thold['data_source_name'] . ']';
break;
case 'created':
$thold = db_fetch_row_prepared('SELECT *
FROM thold_data
WHERE id = ?',
[$id]);
$tname = db_fetch_cell_prepared('SELECT name
FROM data_template
WHERE id = ?',
[$thold['data_template_id']]);
$desc = "Threshold Created by User[$user] TH[$id]";
$desc .= ' DataTemplate[' . $tname . ']';
$desc .= ' DataSource[' . $thold['data_source_name'] . ']';
break;
case 'deleted':
$thold = db_fetch_row_prepared('SELECT *
FROM thold_data
WHERE id = ?',
[$id]);
$tname = db_fetch_cell_prepared('SELECT name
FROM data_template
WHERE id = ?',
[$thold['data_template_id']]);
$desc = "Threshold Deleted by User[$user] TH[$id]";
$desc .= ' DataTemplate[' . $tname . ']';
$desc .= ' DataSource[' . $thold['data_source_name'] . ']';
if (cacti_sizeof($message)) {
$desc .= ' Note[' . implode(', ', $message) . ']';
}
break;
case 'deleted_template':
$thold = db_fetch_row_prepared('SELECT *
FROM thold_template
WHERE id = ?',
[$id]);
$desc = "Template Deleted by User[$user] TH[$id]";
$desc .= ' DataTemplate[' . $thold['data_template_name'] . ']';
$desc .= ' DataSource[' . $thold['data_source_name'] . ']';
break;
case 'modified':
$thold = db_fetch_row_prepared('SELECT *
FROM thold_data
WHERE id = ?',
[$id]);
$rows = db_fetch_assoc_prepared('SELECT ptc.data
FROM plugin_thold_contacts AS ptc
INNER JOIN plugin_thold_threshold_contact AS pttc
WHERE ptc.id=pttc.contact_id
AND pttc.thold_id = ?',
[$id]);
$alert_emails = '';
if (read_config_option('thold_disable_legacy') != 'on') {
$alert_emails = [];
if (count($rows)) {
foreach ($rows as $row) {
$alert_emails[] = $row['data'];
}
}
$alert_emails = implode(',', $alert_emails);
if ($alert_emails != '') {
$alert_emails .= ',' . $thold['notify_extra'];
} else {
$alert_emails = $thold['notify_extra'];
}
}
$list = get_thold_notification_emails($thold['notify_alert']);
if ($list != '') {
if ($alert_emails != '') {
$alert_emails .= ',' . $list;
} else {
$alert_emails .= $list;
}
}
$warning_emails = '';
if (read_config_option('thold_disable_legacy') != 'on') {
$warning_emails = $thold['notify_warning_extra'];
}
$list = get_thold_notification_emails($thold['notify_warning']);
if ($list != '') {
if ($warning_emails != '') {
$warning_emails .= ',' . $list;
} else {
$warning_emails .= $list;
}
}
if ($message['id'] > 0) {
$desc = "Threshold Modified by User[$user] TH[$id]";
} else {
$desc = "Threshold Created by User[$user] TH[$id]";
}
$tname = db_fetch_cell_prepared('SELECT name
FROM data_template
WHERE id = ?',
[$thold['data_template_id']]);
$desc .= ' DataTemplate[' . $tname . ']';
$desc .= ' DataSource[' . $thold['data_source_name'] . ']';
if ($message['template_enabled'] == 'on') {
$desc .= ' Use Template[On]';
} else {
if (isset($thold_types[$thold['thold_type']])) {
$desc .= ' Type Type[' . $thold_types[$thold['thold_type']] . ']';
}
$desc .= ' Enabled: ' . ($thold['thold_enabled'] == 'on' && $thold['thold_per_enabled'] == 'on' ? 'on' : 'off');
switch ($message['thold_type']) {
case 0:
$desc .= ' High[' . $message['thold_hi'] . ']';
$desc .= ' Low[' . $message['thold_low'] . ']';
$desc .= ' Trigger[' . plugin_thold_duration_convert($thold['local_data_id'], $message['thold_fail_trigger'], 'alert') . ']';
$desc .= ' WarnHigh[' . $message['thold_warning_hi'] . ']';
$desc .= ' WarnLow[' . $message['thold_warning_low'] . ']';
$desc .= ' WarnTrigger[' . plugin_thold_duration_convert($thold['local_data_id'], $message['thold_warning_fail_trigger'], 'alert') . ']';
break;
case 1:
$desc .= ' BL Type[' . (!isset($message['bl_type']) ? 0 : $message['bl_type']) . ']';
if ($message['bl_type'] == 1) {
$desc .= ' BL CF[' . (!isset($message['bl_cf']) ? 'AVG' : $message['bl_cf']) . ']';
}
$desc .= ' Range[' . $message['bl_ref_time_range'] . ']';
$desc .= ' DevUp[' . $message['bl_pct_up'] . ']';
$desc .= ' DevDown[' . $message['bl_pct_down'] . ']';
$desc .= ' Trigger[' . $message['bl_fail_trigger'] . ']';
break;
case 2:
$desc .= ' High[' . $message['time_hi'] . ']';
$desc .= ' Low[' . $message['time_low'] . ']';
$desc .= ' Trigger[' . $message['time_fail_trigger'] . ']';
$desc .= ' Time: ' . plugin_thold_duration_convert($thold['local_data_id'], $message['time_fail_length'], 'time') . ']';
$desc .= ' WarnHigh[' . $message['time_warning_hi'] . ']';
$desc .= ' WarnLow[' . $message['time_warning_low'] . ']';
$desc .= ' WarnTrigger[' . $message['time_warning_fail_trigger'] . ']';
$desc .= ' WarnTime[' . plugin_thold_duration_convert($thold['local_data_id'], $message['time_warning_fail_length'], 'time') . ']';
break;
}
$desc .= ' CDEF[' . $message['cdef'] . ']';
$desc .= ' ReAlert[' . plugin_thold_duration_convert($thold['local_data_id'], $message['repeat_alert'], 'alert') . ']';
$desc .= ' AlertEmails[' . $alert_emails . ']';
$desc .= ' WarnEmails[' . $warning_emails . ']';
}
break;
case 'modified_template':
$thold = db_fetch_row_prepared('SELECT *
FROM thold_template
WHERE id = ?',
[$id]);
$rows = db_fetch_assoc_prepared('SELECT ptc.data
FROM plugin_thold_contacts AS ptc
INNER JOIN plugin_thold_template_contact AS pttc
ON ptc.id=pttc.contact_id
WHERE pttc.template_id = ?',
[$id]);
$alert_emails = '';
if (read_config_option('thold_disable_legacy') != 'on') {
$alert_emails = [];
if (count($rows)) {
foreach ($rows as $row) {
$alert_emails[] = $row['data'];
}
}
$alert_emails = implode(',', $alert_emails);
if ($alert_emails != '') {
$alert_emails .= ',' . $thold['notify_extra'];
} else {
$alert_emails = $thold['notify_extra'];
}
}
$list = get_thold_notification_emails($thold['notify_alert']);
if ($list != '') {
if ($alert_emails != '') {
$alert_emails .= ',' . $list;
} else {
$alert_emails .= $list;
}
}
$warning_emails = '';
if (read_config_option('thold_disable_legacy') != 'on') {
$warning_emails = $thold['notify_warning_extra'];
}
$list = get_thold_notification_emails($thold['notify_warning']);
if ($list != '') {
if ($warning_emails != '') {
$warning_emails .= ',' . $list;
} else {
$warning_emails .= $list;
}
}
if ($message['id'] > 0) {
$desc = "Template Modified by User[$user] TT[$id]";
} else {
$desc = "Template Created by User[$user] TT[$id]";
}
$desc .= ' DataTemplate[' . $thold['data_template_name'] . ']';
$desc .= ' DataSource[' . $thold['data_source_name'] . ']';
if (isset($thold_types[$thold['thold_type']])) {
$desc .= ' Thold Type[' . $thold_types[$message['thold_type']] . ']';
}
$desc .= ' Enabled: ' . ($thold['thold_enabled'] == 'on' ? 'on' : 'off');
switch ($message['thold_type']) {
case 0:
$desc .= ' High[' . (isset($message['thold_hi']) ? $message['thold_hi'] : '') . ']';
$desc .= ' Low[' . (isset($message['thold_low']) ? $message['thold_low'] : '') . ']';
$desc .= ' Trigger[' . plugin_thold_duration_convert($thold['data_template_id'], (isset($message['thold_fail_trigger']) ? $message['thold_fail_trigger'] : ''), 'alert', 'data_template_id') . ']';
$desc .= ' WarnHigh[' . (isset($message['thold_warning_hi']) ? $message['thold_warning_hi'] : '') . ']';
$desc .= ' WarnLow[' . (isset($message['thold_warning_low']) ? $message['thold_warning_low'] : '') . ']';
$desc .= ' WarnTrigger[' . plugin_thold_duration_convert($thold['data_template_id'], (isset($message['thold_warning_fail_trigger']) ? $message['thold_warning_fail_trigger'] : ''), 'alert', 'data_template_id') . ']';
break;
case 1:
$desc .= ' BL Type[' . (!isset($message['bl_type']) ? 0 : $message['bl_type']) . ']';
if ($message['bl_type'] == 1) {
$desc .= ' BL CF[' . (!isset($message['bl_cf']) ? 'AVG' : $message['bl_cf']) . ']';
}
$desc .= ' Range[' . $message['bl_ref_time_range'] . ']';
$desc .= ' DevUp[' . (isset($message['bl_pct_up']) ? $message['bl_pct_up'] : '') . ']';
$desc .= ' DevDown[' . (isset($message['bl_pct_down']) ? $message['bl_pct_down'] : '') . ']';
$desc .= ' Trigger[' . $message['bl_fail_trigger'] . ']';
break;
case 2:
$desc .= ' High[' . $message['time_hi'] . ']';
$desc .= ' Low[' . $message['time_low'] . ']';
$desc .= ' Trigger[' . $message['time_fail_trigger'] . ']';
$desc .= ' Time[' . plugin_thold_duration_convert($thold['data_template_id'], $message['time_fail_length'], 'alert', 'data_template_id') . ']';
$desc .= ' WarnHigh[' . $message['time_warning_hi'] . ']';
$desc .= ' WarnLow[' . $message['time_warning_low'] . ']';
$desc .= ' WarnTrigger[' . $message['time_warning_fail_trigger'] . ']';
$desc .= ' WarnTime[' . plugin_thold_duration_convert($thold['data_template_id'], $message['time_warning_fail_length'], 'alert', 'data_template_id') . ']';
break;
}
$desc .= ' CDEF[' . (isset($message['cdef']) ? $message['cdef'] : '') . ']';
$desc .= ' ReAlert[' . plugin_thold_duration_convert($thold['data_template_id'], $message['repeat_alert'], 'alert', 'data_template_id') . ']';
$desc .= ' AlertEmails[' . $alert_emails . ']';
$desc .= ' WarnEmails[' . $warning_emails . ']';
break;
}
if ($desc != '') {
thold_cacti_log($desc);
}
}
/**
* get_thold_severity - derives a threshold's display severity (normal, alert, warning, notice,
* ack-required, disabled, or baseline) from its current hi/lo, time-based, or baseline fail
* counters and trigger settings.
*
* @param array &$td The threshold row (thold_type, fail counts/triggers, hi/lo/time bounds,
* acknowledgment, enabled flags).
*
* @return int One of the THOLD_SEVERITY_* constants.
*/
function get_thold_severity(&$td) {
$severity = THOLD_SEVERITY_NORMAL;
if (($td['template_enabled'] == 'on' && $td['thold_enabled'] == 'off') || $td['thold_per_enabled'] == '') {
return THOLD_SEVERITY_DISABLED;
}
if (!isset($td['thold_type'])) {
return THOLD_SEVERITY_NORMAL;
}
switch($td['thold_type']) {
case '0': // Hi/Low
if ($td['thold_alert'] != 0) {
if ($td['thold_hi'] != '' || $td['thold_low'] != '') {
if ($td['thold_fail_count'] >= $td['thold_fail_trigger']) {
$severity = THOLD_SEVERITY_ALERT;
} elseif ($td['lastread'] > $td['thold_hi'] && $td['thold_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_ALERT;
} elseif ($td['lastread'] < $td['thold_low'] && $td['thold_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_ALERT;
} elseif ($td['thold_warning_hi'] != '' || $td['thold_warning_low'] != '') {
if ($td['thold_warning_fail_count'] >= $td['thold_warning_fail_trigger']) {
$severity = THOLD_SEVERITY_WARNING;
} elseif ($td['lastread'] > $td['thold_warning_hi'] && $td['thold_warning_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_WARNING;
} elseif ($td['lastread'] < $td['thold_warning_low'] && $td['thold_warning_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_WARNING;
} else {
$severity = THOLD_SEVERITY_NOTICE;
}
} else {
$severity = THOLD_SEVERITY_NOTICE;
}
} elseif ($td['thold_warning_hi'] != '' || $td['thold_warning_low'] != '') {
if ($td['thold_warning_fail_count'] >= $td['thold_warning_fail_trigger']) {
$severity = THOLD_SEVERITY_WARNING;
} elseif ($td['lastread'] > $td['thold_warning_hi'] && $td['thold_warning_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_WARNING;
} elseif ($td['lastread'] < $td['thold_warning_low'] && $td['thold_warning_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_WARNING;
} else {
$severity = THOLD_SEVERITY_NOTICE;
}
}
} elseif ($td['acknowledgment'] == 'on') {
$severity = THOLD_SEVERITY_ACKREQ;
}
break;
case '1': // Baseline
if ($td['bl_alert'] == 1) {
if ($td['bl_fail_count'] >= $td['bl_fail_trigger']) {
$severity = THOLD_SEVERITY_BASELINE;
} else {
$severity = THOLD_SEVERITY_NOTICE;
}
} elseif ($td['bl_alert'] == 2) {
if ($td['bl_fail_count'] >= $td['bl_fail_trigger']) {
$severity = THOLD_SEVERITY_BASELINE;
} else {
$severity = THOLD_SEVERITY_NOTICE;
}
} elseif ($td['acknowledgment'] == 'on') {
$severity = THOLD_SEVERITY_ACKREQ;
}
break;
case '2': // Time Based
if ($td['thold_alert'] != 0) {
if ($td['time_hi'] != '' || $td['time_low'] != '') {
if ($td['thold_fail_count'] >= $td['time_fail_trigger']) {
$severity = THOLD_SEVERITY_ALERT;
} elseif ($td['lastread'] > $td['time_hi'] && $td['time_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_ALERT;
} elseif ($td['lastread'] < $td['time_low'] && $td['time_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_ALERT;
} elseif ($td['time_warning_hi'] != '' || $td['time_warning_low']) {
if ($td['thold_warning_fail_count'] >= $td['time_warning_fail_trigger']) {
$severity = THOLD_SEVERITY_WARNING;
} elseif ($td['lastread'] > $td['time_warning_hi'] && $td['time_warning_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_WARNING;
} elseif ($td['lastread'] < $td['time_warning_low'] && $td['time_warning_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_WARNING;
} else {
$severity = THOLD_SEVERITY_NOTICE;
}
} else {
$severity = THOLD_SEVERITY_NOTICE;
}
} elseif ($td['time_warning_hi'] != '' || $td['time_warning_low']) {
if ($td['thold_warning_fail_count'] >= $td['time_warning_fail_trigger']) {
$severity = THOLD_SEVERITY_WARNING;
} elseif ($td['lastread'] > $td['time_warning_hi'] && $td['time_warning_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_WARNING;
} elseif ($td['lastread'] < $td['time_warning_low'] && $td['time_warning_fail_trigger'] == 0) {
$severity = THOLD_SEVERITY_WARNING;
} else {
$severity = THOLD_SEVERITY_NOTICE;
}
}
} elseif ($td['acknowledgment'] == 'on') {
$severity = THOLD_SEVERITY_ACKREQ;
}
break;
default:
break;
}
return $severity;
}
/**
* thold_severity_sql - returns a SQL expression (over the `thold_data td` alias) that computes a
* threshold's status as a legend-order rank, mirroring get_thold_severity(): 1=Alert,
* 2=Baseline Alert, 3=Warning, 4=Notice, 5=Ok, 6=Acknowledgment, 7=Disabled. Used to make the
* status-pill column sortable and to drive the severity options in the Status filter.
*
* @return string The CASE expression (parenthesised, no alias) for use in ORDER BY / WHERE.
*/
function thold_severity_sql() {
return <<<'SQL'
(CASE
WHEN ((td.template_enabled = 'on' AND td.thold_enabled = 'off') OR td.thold_per_enabled = '') THEN 7
WHEN td.thold_type = 0 THEN (CASE
WHEN td.thold_alert != 0 THEN (CASE
WHEN (td.thold_hi != '' OR td.thold_low != '') THEN (CASE
WHEN td.thold_fail_count >= td.thold_fail_trigger THEN 1
WHEN (td.thold_hi != '' AND (td.lastread + 0.0) > (td.thold_hi + 0.0) AND td.thold_fail_trigger = 0) THEN 1
WHEN (td.thold_low != '' AND (td.lastread + 0.0) < (td.thold_low + 0.0) AND td.thold_fail_trigger = 0) THEN 1
WHEN (td.thold_warning_hi != '' OR td.thold_warning_low != '') THEN (CASE
WHEN td.thold_warning_fail_count >= td.thold_warning_fail_trigger THEN 3
WHEN (td.thold_warning_hi != '' AND (td.lastread + 0.0) > (td.thold_warning_hi + 0.0) AND td.thold_warning_fail_trigger = 0) THEN 3
WHEN (td.thold_warning_low != '' AND (td.lastread + 0.0) < (td.thold_warning_low + 0.0) AND td.thold_warning_fail_trigger = 0) THEN 3
ELSE 4 END)
ELSE 4 END)
WHEN (td.thold_warning_hi != '' OR td.thold_warning_low != '') THEN (CASE
WHEN td.thold_warning_fail_count >= td.thold_warning_fail_trigger THEN 3
WHEN (td.thold_warning_hi != '' AND (td.lastread + 0.0) > (td.thold_warning_hi + 0.0) AND td.thold_warning_fail_trigger = 0) THEN 3
WHEN (td.thold_warning_low != '' AND (td.lastread + 0.0) < (td.thold_warning_low + 0.0) AND td.thold_warning_fail_trigger = 0) THEN 3
ELSE 4 END)
ELSE 5 END)
WHEN td.acknowledgment = 'on' THEN 6
ELSE 5 END)
WHEN td.thold_type = 1 THEN (CASE
WHEN td.bl_alert IN (1, 2) THEN (CASE WHEN td.bl_fail_count >= td.bl_fail_trigger THEN 2 ELSE 4 END)
WHEN td.acknowledgment = 'on' THEN 6
ELSE 5 END)
WHEN td.thold_type = 2 THEN (CASE
WHEN td.thold_alert != 0 THEN (CASE
WHEN (td.time_hi != '' OR td.time_low != '') THEN (CASE
WHEN td.thold_fail_count >= td.time_fail_trigger THEN 1
WHEN (td.time_hi != '' AND (td.lastread + 0.0) > (td.time_hi + 0.0) AND td.time_fail_trigger = 0) THEN 1
WHEN (td.time_low != '' AND (td.lastread + 0.0) < (td.time_low + 0.0) AND td.time_fail_trigger = 0) THEN 1
WHEN (td.time_warning_hi != '' OR td.time_warning_low NOT IN ('', '0')) THEN (CASE
WHEN td.thold_warning_fail_count >= td.time_warning_fail_trigger THEN 3
WHEN (td.time_warning_hi != '' AND (td.lastread + 0.0) > (td.time_warning_hi + 0.0) AND td.time_warning_fail_trigger = 0) THEN 3
WHEN (td.time_warning_low != '' AND (td.lastread + 0.0) < (td.time_warning_low + 0.0) AND td.time_warning_fail_trigger = 0) THEN 3
ELSE 4 END)
ELSE 4 END)
WHEN (td.time_warning_hi != '' OR td.time_warning_low NOT IN ('', '0')) THEN (CASE
WHEN td.thold_warning_fail_count >= td.time_warning_fail_trigger THEN 3
WHEN (td.time_warning_hi != '' AND (td.lastread + 0.0) > (td.time_warning_hi + 0.0) AND td.time_warning_fail_trigger = 0) THEN 3
WHEN (td.time_warning_low != '' AND (td.lastread + 0.0) < (td.time_warning_low + 0.0) AND td.time_warning_fail_trigger = 0) THEN 3
ELSE 4 END)
ELSE 5 END)
WHEN td.acknowledgment = 'on' THEN 6
ELSE 5 END)
ELSE 5 END)
SQL;
}
/**
* thold_datasource_required - checks whether a data source name already embeds its own
* `[data_source]` suffix, when the 'show_datasource' setting is enabled.
*
* @param string $name The label/name string to check for an existing `[data_source]` suffix.
* @param string $data_source The data source name to check for.
*
* @return bool True when the data source suffix still needs to be appended by the caller.
*/
function thold_datasource_required($name, $data_source) {
$show_datasource = read_config_option('show_datasource');
if ($show_datasource == 'on') {
if (strstr($name, "[$data_source]") !== false) {
return false;
}
} else {
return false;
}
return true;
}
/**
* Deliver one notification to one recipient list.
*
* The eighteen send sites in thold_check_threshold() all resolve the list's
* format file, then mail the list unless it is empty or the threshold has been
* acknowledged. The message body is built inside that guard rather than by the
* caller, because building it queries and an empty recipient list should not
* pay for a message nobody receives.
*
* @param string $recipients Comma separated addresses, possibly empty.
* @param string $bcc Comma separated blind addresses.
* @param string $subject Subject line, already composed.
* @param string $text_type alert, warning or restoral.
* @param int $list_id Notification list supplying the format.
* @param array<string, mixed> $file_array Graph attachment, or empty for none.
* @param array<string, mixed> $thold_data Threshold row.
* @param array<string, mixed> $h Device row.
* @param int $timespan Graph timespan for the attachment.
*
* @return string The message that was sent, or '' when nothing was.
*/
function thold_mail_notification($recipients, $bcc, $subject, $text_type, $list_id, $file_array, &$thold_data, &$h, $timespan = 7) {
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $list_id);
if (trim($recipients) == '' || $thold_data['acknowledgment'] != '') {
return '';
}
switch ($text_type) {
case 'alert':
$message = get_thold_alert_text($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id']);
break;
case 'warning':
$message = get_thold_warning_text($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id']);
break;
default:
$message = get_thold_restoral_text($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id']);
break;
}
thold_mail($recipients, $bcc, '', $subject, $message, $file_array, '', $list_id, $h, $format_file, $timespan);
return $message;
}
/**
* thold_evaluation_context - assembles the shared settings/notification-address/graph-attachment
* context thold_check_threshold() needs once per threshold evaluation (syslog/SNMP-trap toggles,
* alert/warning trigger overrides, resolved alert/warning recipient lists, and an optional graph
* image attachment array).
*
* @param array $thold_data The threshold row being evaluated.
*
* @return array The assembled evaluation context, keyed by setting/recipient-list name.
*/
function thold_evaluation_context(array $thold_data) {
$alert_trigger = read_config_option('alert_trigger');
$httpurl = read_config_option('base_url');
$thold_send_text_only = read_config_option('thold_send_text_only');
// see if we have two notification lists or one
$notify_different = $thold_data['notify_warning'] > 0
&& $thold_data['notify_warning'] != $thold_data['notify_alert']
&& read_config_option('thold_notify_alerts_to_warning_recipients') == 'on';
$file_array = [];
if ($thold_send_text_only != 'on' && !empty($thold_data['local_graph_id'])) {
$file_array = [
'local_graph_id' => $thold_data['local_graph_id'],
'local_data_id' => $thold_data['local_data_id'],
'rra_id' => 0,
'file' => "$httpurl/graph_image.php?local_graph_id=" . $thold_data['local_graph_id'] . '&rra_id=0&view_type=tree',
'mimetype' => 'image/png',
'filename' => clean_up_name(thold_get_cached_name($thold_data))
];
}
return [
// Settings for syslogging
'syslog' => $thold_data['syslog_enabled'] == 'on',
'syslog_priority' => $thold_data['syslog_priority'],
'syslog_facility' => $thold_data['syslog_facility'],
'realert' => read_config_option('alert_repeat'),
'alert_bl_trigger' => read_config_option('alert_bl_trigger'),
'thold_snmp_traps' => read_config_option('thold_alert_snmp') == 'on',
'thold_snmp_warning_traps' => read_config_option('thold_alert_snmp_warning') == 'on',
'thold_snmp_normal_traps' => read_config_option('thold_alert_snmp_normal') == 'on',
'cacti_polling_interval' => read_config_option('poller_interval'),
// An unset trigger on the threshold falls back to the global default.
'trigger' => $thold_data['thold_fail_trigger'] == '' ? $alert_trigger : $thold_data['thold_fail_trigger'],
'warning_trigger' => $thold_data['thold_warning_fail_trigger'] == '' ? $alert_trigger : $thold_data['thold_warning_fail_trigger'],
'alertstat' => $thold_data['thold_alert'],
'notify_different' => $notify_different,
'file_array' => $file_array,
'url' => $httpurl . '/graph.php?local_graph_id=' . $thold_data['local_graph_id'] . '&rra_id=all',
'lastread' => $thold_data['lastread'],
'alert_emails' => get_thold_emails($thold_data, 'alert', 'to'),
'alert_bcc_emails' => get_thold_emails($thold_data, 'alert', 'bcc'),
'warning_emails' => get_thold_emails($thold_data, 'warning', 'to'),
'warning_bcc_emails' => get_thold_emails($thold_data, 'warning', 'bcc'),
];
}
/**
* thold_check_threshold - the core per-poll-cycle threshold evaluator. Applies any configured
* CDEF value modification, evaluates the threshold's hi/lo, time-based, or baseline condition
* against the current sample, advances/resets its fail and warning-fail counters, and on a state
* transition updates thold_data's alert state, sends alert/warning/restoral notifications (email,
* SNMP trap, syslog) via thold_mail_notification(), and records the transition via thold_log()/
* plugin_thold_log_changes().
*
* @param array &$thold_data The threshold row to evaluate, updated in place with the new alert
* state and fail counters.
*
* @global array $config Cacti's global configuration array.
* @global array $plugins The list of currently-enabled plugins.
* @global bool $debug Whether debug logging is enabled for this request.
*
* @return void
*/
function thold_check_threshold(&$thold_data) {
global $config, $plugins, $debug;
// Modify critical thold values based upon a cdef if present
thold_modify_values_by_cdef($thold_data);
thold_debug('Checking Threshold:' .
' id: ' . var_export($thold_data['id'], true) .
', name: ' . var_export($thold_data['name_cache'], true) .
', data source: ' . var_export($thold_data['data_source_name'], true) .
', local_data_id: ' . var_export($thold_data['local_data_id'], true) .
', data_template_rrd_id: ' . var_export($thold_data['data_template_rrd_id'], true) .
', value: ' . var_export($thold_data['lastread'], true));
// Do not proceed if we have chosen to globally disable all alerts
if (read_config_option('thold_disable_all') == 'on') {
thold_debug('Threshold checking is disabled globally');
return;
}
// An unavailable sample is not evidence that an active alert recovered.
if (!is_numeric($thold_data['lastread'])) {
return;
}
$alert_exempt = read_config_option('alert_exempt');
// check for exemptions
$weekday = date('l');
if (($weekday == 'Saturday' || $weekday == 'Sunday') && $alert_exempt == 'on') {
thold_debug('Threshold checking is disabled by global weekend exemption');
return;
}
// check for the weekend exemption on the threshold level
if (($weekday == 'Saturday' || $weekday == 'Sunday') && $thold_data['exempt'] == 'on') {
thold_debug('Threshold checking is disabled by global weekend exemption');
return;
}
// don't alert for this host if it's selected for maintenance
if (api_plugin_is_enabled('maint') || in_array('maint', $plugins, true)) {
if (file_exists($config['base_path'] . '/plugins/maint/functions.php')) {
require_once($config['base_path'] . '/plugins/maint/functions.php');
} else if (file_exists($config['base_path'] . '/plugins/maint/includes/functions.php')) {
require_once($config['base_path'] . '/plugins/maint/includes/functions.php');
} else {
cacti_log('ERROR: cannot include maint functions', false, 'THOLD');
}
}
if (api_plugin_is_enabled('maint') && function_exists('plugin_maint_check_cacti_host') && plugin_maint_check_cacti_host($thold_data['host_id'])) {
$maint_dev = true;
} else {
$maint_dev = false;
}
$local_graph_id = $thold_data['local_graph_id'];
$h = [];
if (isset($thold_data['host_id']) && $thold_data['host_id'] > 0) {
// only alert if Device is in UP mode (not down, unknown, or recovering)
$h = db_fetch_row_prepared('SELECT *
FROM host
WHERE id = ?',
[$thold_data['host_id']]);
if (cacti_sizeof($h) && $h['status'] != 3) {
thold_debug('Threshold checking halted by Device Status (' . $h['status'] . ')');
return;
}
$h['thold_id'] = $thold_data['id'];
} else {
// function called during polling
$h['id'] = $thold_data['host_id'];
$h['snmp_engine_id'] = $thold_data['snmp_engine_id'];
$h['description'] = $thold_data['description'];
$h['hostname'] = $thold_data['hostname'];
$h['thold_id'] = $thold_data['id'];
}
// ensure that Cacti will make of individual defined SNMP Engine IDs
$overwrite['snmp_engine_id'] = $h['snmp_engine_id'];
$context = thold_evaluation_context($thold_data);
$syslog = $context['syslog'];
$syslog_priority = $context['syslog_priority'];
$syslog_facility = $context['syslog_facility'];
$realert = $context['realert'];
$alert_bl_trigger = $context['alert_bl_trigger'];
$thold_snmp_traps = $context['thold_snmp_traps'];
$thold_snmp_warning_traps = $context['thold_snmp_warning_traps'];
$thold_snmp_normal_traps = $context['thold_snmp_normal_traps'];
$cacti_polling_interval = $context['cacti_polling_interval'];
$trigger = $context['trigger'];
$warning_trigger = $context['warning_trigger'];
$alertstat = $context['alertstat'];
$notify_different = $context['notify_different'];
$file_array = $context['file_array'];
$url = $context['url'];
$lastread = $context['lastread'];
$alert_emails = $context['alert_emails'];
$alert_bcc_emails = $context['alert_bcc_emails'];
$warning_emails = $context['warning_emails'];
$warning_bcc_emails = $context['warning_bcc_emails'];
switch ($thold_data['thold_type']) {
case 0: // hi/low
if (is_numeric($thold_data['lastread'])) {
$breach_up = ($thold_data['thold_hi'] != '' && $thold_data['lastread'] > $thold_data['thold_hi']);
$breach_down = ($thold_data['thold_low'] != '' && $thold_data['lastread'] < $thold_data['thold_low']);
$warning_breach_up = ($thold_data['thold_warning_hi'] != '' && $thold_data['lastread'] > $thold_data['thold_warning_hi']);
$warning_breach_down = ($thold_data['thold_warning_low'] != '' && $thold_data['lastread'] < $thold_data['thold_warning_low']);
} else {
$breach_up = $breach_down = $warning_breach_up = $warning_breach_down = false;
}
// is in alert status
if ($breach_up || $breach_down) {
$notify = false;
thold_debug('Threshold HI / Low check breached HI:' . $thold_data['thold_hi'] . ' LOW:' . $thold_data['thold_low'] . ' VALUE:' . $thold_data['lastread']);
if (!$maint_dev) {
$thold_data['thold_fail_count']++;
}
$thold_data['thold_alert'] = ($breach_up ? STAT_HI : STAT_LO);
// Re-Alert?
$ra = ($thold_data['thold_fail_count'] > $trigger && $thold_data['repeat_alert'] != 0 && $thold_data['thold_fail_count'] % $thold_data['repeat_alert'] == 0);
if (($thold_data['thold_fail_count'] == $trigger || $ra) && !$maint_dev) {
$notify = true;
}
if ($notify && !$ra) {
db_execute_prepared('UPDATE thold_data
SET lastchanged = NOW()
WHERE id = ?',
[$thold_data['id']]);
if ($thold_data['persist_ack'] == 'on' || $thold_data['reset_ack'] == 'on') {
db_execute_prepared('UPDATE thold_data
SET acknowledgment = "on"
WHERE id = ?',
[$thold_data['id']]);
}
}
// If this is a realert and the operator has reset the ack, don't notify
if ($notify && $ra && $thold_data['reset_ack'] == 'on' && $thold_data['acknowledgment'] == '') {
$suspend_notify = true;
} else {
$suspend_notify = false;
}
$subject = get_email_subject('ALERT', false, $lastread, $ra, $breach_up, $thold_data);
if ($notify) {
if (!$suspend_notify && !$maint_dev) {
thold_debug('Alerting is necessary');
$notify_list_id = $thold_data['notify_alert'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if ($syslog) {
logger($subject, $url, $syslog_priority, $syslog_facility);
}
if (trim($alert_emails) != '' && $thold_data['acknowledgment'] == '') {
$message = get_thold_alert_text($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id']);
thold_mail($alert_emails, $alert_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file);
}
if ($notify_different) {
$notify_list_id = $thold_data['notify_warning'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if (trim($warning_emails) != '' && $thold_data['acknowledgment'] == '') {
thold_mail($warning_emails, $warning_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file);
}
}
thold_command_execution($thold_data, $h, $breach_up, $breach_down);
$save = [
'class' => 'alert',
'thold_data' => $thold_data,
'subject' => $subject,
'repeat_alert' => $ra,
'host_data' => $h,
'breach_up' => $breach_up,
'breach_down' => $breach_down,
'warning_breach_up' => $warning_breach_up,
'warning_breach_down' => $warning_breach_down
];
api_plugin_hook_function('thold_action', $save);
if ($thold_snmp_traps) {
$thold_snmp_data = get_thold_snmp_data($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread']);
$thold_snmp_data['eventClass'] = 3;
$thold_snmp_data['eventSeverity'] = $thold_data['snmp_event_severity'];
$thold_snmp_data['eventStatus'] = $thold_data['thold_alert'] + 1;
$thold_snmp_data['eventRealertStatus'] = ($ra ? ($breach_up ? 3 : 2) : 1);
$thold_snmp_data['eventNotificationType'] = ($ra ? ST_NOTIFYRA : ST_NOTIFYAL) + 1;
$thold_snmp_data['eventFailCount'] = $thold_data['thold_fail_count'];
$thold_snmp_data['eventFailDuration'] = $thold_data['thold_fail_count'] * $cacti_polling_interval;
$thold_snmp_data['eventFailDurationTrigger'] = $trigger * $cacti_polling_interval;
$thold_snmp_data['eventDeviceIp'] = gethostbyname($h['hostname']);
$thold_snmp_data['eventDescription'] = str_replace(
['<FAIL_COUNT>', '<FAIL_DURATION>'],
[$thold_snmp_data['eventFailCount'], $thold_snmp_data['eventFailDuration']],
$thold_snmp_data['eventDescription']
);
thold_snmptrap($thold_snmp_data, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
}
}
thold_log([
'type' => 0,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => ($breach_up ? $thold_data['thold_hi'] : $thold_data['thold_low']),
'current' => $thold_data['lastread'],
'status' => ($ra ? ST_NOTIFYRA : ST_NOTIFYAL),
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
}
db_execute_prepared('UPDATE thold_data
SET thold_alert = ?,
thold_fail_count = ?,
thold_warning_fail_count = 0
WHERE id = ?',
[$thold_data['thold_alert'], $thold_data['thold_fail_count'], $thold_data['id']]);
} elseif ($warning_breach_up || $warning_breach_down) {
$notify = false;
thold_debug('Threshold HI / Low Warning check breached HI:' . $thold_data['thold_warning_hi'] .
' LOW:' . $thold_data['thold_warning_low'] .
' VALUE:' . $thold_data['lastread'] .
' TRIGGER:' . $trigger .
' WRNCNT:' . $thold_data['thold_warning_fail_count']);
if (!$maint_dev) {
$thold_data['thold_warning_fail_count']++;
}
$thold_data['thold_alert'] = ($warning_breach_up ? STAT_HI : STAT_LO);
// re-alert?
$ra = ($thold_data['thold_warning_fail_count'] > $warning_trigger && $thold_data['repeat_alert'] != 0 && $thold_data['thold_warning_fail_count'] % $thold_data['repeat_alert'] == 0);
if (($thold_data['thold_warning_fail_count'] == $warning_trigger || $ra) && !$maint_dev) {
$notify = true;
}
if ($notify && !$ra) {
db_execute_prepared('UPDATE thold_data
SET lastchanged = NOW()
WHERE id = ?',
[$thold_data['id']]);
if ($thold_data['persist_ack'] == 'on' || $thold_data['reset_ack'] == 'on') {
db_execute_prepared('UPDATE thold_data
SET acknowledgment = "on"
WHERE id = ?',
[$thold_data['id']]);
}
}
// If this is a realert and the operator has reset the ack, don't notify
if ($notify && $ra && $thold_data['reset_ack'] == 'on' && $thold_data['acknowledgment'] == '') {
$suspend_notify = true;
} else {
$suspend_notify = false;
}
if ($notify) {
$etrigger = false;
} else {
$etrigger = true;
}
$subject = get_email_subject('WARNING', $etrigger, $lastread, $ra, $warning_breach_up, $thold_data);
if ($notify) {
if (!$suspend_notify && !$maint_dev) {
thold_debug('Alerting is necessary');
if ($syslog) {
logger($subject, $url, $syslog_priority, $syslog_facility);
}
$message = thold_mail_notification($warning_emails, $warning_bcc_emails, $subject, 'warning', $thold_data['notify_warning'], $file_array, $thold_data, $h, $thold_data['graph_timespan']);
$save = [
'class' => 'warn',
'thold_data' => $thold_data,
'subject' => $subject,
'repeat_alert' => $ra,
'host_data' => $h,
'breach_up' => $breach_up,
'breach_down' => $breach_down,
'warning_breach_up' => $warning_breach_up,
'warning_breach_down' => $warning_breach_down
];
api_plugin_hook_function('thold_action', $save);
if ($thold_snmp_traps && $thold_snmp_warning_traps) {
$thold_snmp_data = get_thold_snmp_data($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread']);
$thold_snmp_data['eventClass'] = 2;
$thold_snmp_data['eventSeverity'] = $thold_data['snmp_event_warning_severity'];
$thold_snmp_data['eventStatus'] = $thold_data['thold_alert'] + 1;
$thold_snmp_data['eventRealertStatus'] = ($ra ? ($warning_breach_up ? 3 : 2) : 1);
$thold_snmp_data['eventNotificationType'] = ($ra ? ST_NOTIFYRAW : ST_NOTIFYWA) + 1;
$thold_snmp_data['eventFailCount'] = $thold_data['thold_warning_fail_count'];
$thold_snmp_data['eventFailDuration'] = $thold_data['thold_warning_fail_count'] * $cacti_polling_interval;
$thold_snmp_data['eventFailDurationTrigger'] = $warning_trigger * $cacti_polling_interval;
$thold_snmp_data['eventDeviceIp'] = gethostbyname($h['hostname']);
$thold_snmp_data['eventDescription'] = str_replace(
['<FAIL_COUNT>', '<FAIL_DURATION>'],
[$thold_snmp_data['eventFailCount'], $thold_snmp_data['eventFailDuration']],
$thold_snmp_data['eventDescription']
);
thold_snmptrap($thold_snmp_data, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
}
}
thold_log([
'type' => 0,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => ($warning_breach_up ? $thold_data['thold_warning_hi'] : $thold_data['thold_warning_low']),
'current' => $thold_data['lastread'],
'status' => ($ra ? ST_NOTIFYRAW : ST_NOTIFYWA),
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $warning_emails,
'bcc_emails' => $warning_bcc_emails]
);
} elseif (($thold_data['thold_warning_fail_count'] >= $warning_trigger) && ($thold_data['thold_fail_count'] >= $trigger)) {
$subject = get_email_subject('ALERT > WARNING', false, $lastread, $ra, $warning_breach_up, $thold_data);
// If this is a realert and the operator has reset the ack, don't notify
if ($ra && $thold_data['reset_ack'] == 'on' && $thold_data['acknowledgment'] == '') {
$suspend_notify = true;
} else {
$suspend_notify = false;
}
if (!$suspend_notify && !$maint_dev) {
$message = thold_mail_notification($alert_emails, $alert_bcc_emails, $subject, 'warning', $thold_data['notify_alert'], $file_array, $thold_data, $h, $thold_data['graph_timespan']);
if ($notify_different) {
$notify_list_id = $thold_data['notify_warning'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if (trim($warning_emails) != '' && $thold_data['acknowledgment'] == '') {
thold_mail($warning_emails, $warning_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file);
}
}
$save = [
'class' => 'alert2warn',
'thold_data' => $thold_data,
'subject' => $subject,
'repeat_alert' => $ra,
'host_data' => $h,
'breach_up' => $breach_up,
'breach_down' => $breach_down,
'warning_breach_up' => $warning_breach_up,
'warning_breach_down' => $warning_breach_down
];
api_plugin_hook_function('thold_action', $save);
if ($thold_snmp_traps && $thold_snmp_warning_traps) {
$thold_snmp_data = get_thold_snmp_data($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread']);
$thold_snmp_data['eventClass'] = 2;
$thold_snmp_data['eventSeverity'] = $thold_data['snmp_event_warning_severity'];
$thold_snmp_data['eventStatus'] = $thold_data['thold_alert'] + 1;
$thold_snmp_data['eventNotificationType'] = ST_NOTIFYAW + 1;
$thold_snmp_data['eventFailCount'] = $thold_data['thold_warning_fail_count'];
$thold_snmp_data['eventFailDuration'] = $thold_data['thold_warning_fail_count'] * $cacti_polling_interval;
$thold_snmp_data['eventFailDurationTrigger'] = $trigger * $cacti_polling_interval;
$thold_snmp_data['eventDeviceIp'] = gethostbyname($h['hostname']);
$thold_snmp_data['eventDescription'] = str_replace(
['<FAIL_COUNT>', '<FAIL_DURATION>'],
[$thold_snmp_data['eventFailCount'], $thold_snmp_data['eventFailDuration']],
$thold_snmp_data['eventDescription']
);
thold_snmptrap($thold_snmp_data, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
}
}
thold_log([
'type' => 0,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => ($warning_breach_up ? $thold_data['thold_warning_hi'] : $thold_data['thold_warning_low']),
'current' => $thold_data['lastread'],
'status' => ST_NOTIFYAW,
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
}
db_execute_prepared('UPDATE thold_data
SET thold_alert = ?,
thold_warning_fail_count = ?,
thold_fail_count = 0
WHERE id = ?',
[$thold_data['thold_alert'], $thold_data['thold_warning_fail_count'], $thold_data['id']]);
} else {
thold_debug('Threshold HI / Low check is Normal HI:' . $thold_data['thold_hi'] . ' LOW:' . $thold_data['thold_low'] . ' VALUE:' . $thold_data['lastread']);
$subject = get_email_subject('NORMAL', false, $lastread, false, false, $thold_data);
// if we were at an alert status before
if ($alertstat != 0) {
db_execute_prepared('UPDATE thold_data
SET thold_alert = 0,
thold_fail_count = 0,
lastchanged = NOW(),
thold_warning_fail_count=0
WHERE id = ?',
[$thold_data['id']]);
if ($thold_data['reset_ack'] == 'on') {
db_execute_prepared('UPDATE thold_data
SET acknowledgment=""
WHERE id = ?',
[$thold_data['id']]);
}
if ($thold_data['thold_warning_fail_count'] >= $warning_trigger && $thold_data['restored_alert'] != 'on' && !$maint_dev) {
if ($syslog) {
logger($subject, $url, $syslog_priority, $syslog_facility);
}
$notify_list_id = $thold_data['notify_warning'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if (trim($warning_emails) != '' && $thold_data['restored_alert'] != 'on' && $thold_data['acknowledgment'] == '') {
$message = get_thold_restoral_text($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id']);
thold_mail($warning_emails, $warning_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file, $thold_data['graph_timespan']);
}
$save = [
'class' => 'warn2normal',
'thold_data' => $thold_data,
'subject' => $subject,
'host_data' => $h,
'breach_up' => $breach_up,
'breach_down' => $breach_down,
'warning_breach_up' => $warning_breach_up,
'warning_breach_down' => $warning_breach_down
];
api_plugin_hook_function('thold_action', $save);
if ($thold_snmp_traps && $thold_snmp_normal_traps) {
$thold_snmp_data = get_thold_snmp_data($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread']);
$thold_snmp_data['eventClass'] = 1;
$thold_snmp_data['eventSeverity'] = 1;
$thold_snmp_data['eventStatus'] = 1;
$thold_snmp_data['eventNotificationType'] = ST_NOTIFYRS + 1;
$thold_snmp_data['eventDeviceIp'] = gethostbyname($h['hostname']);
thold_snmptrap($thold_snmp_data, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
}
thold_log([
'type' => 0,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => '',
'current' => $thold_data['lastread'],
'status' => ST_NOTIFYRS,
'description' => $subject,
'emails' => $warning_emails,
'bcc_emails' => $warning_bcc_emails]
);
} elseif ($thold_data['thold_fail_count'] >= $trigger && $thold_data['restored_alert'] != 'on' && !$maint_dev) {
if ($syslog) {
logger($subject, $url, $syslog_priority, $syslog_facility);
}
$notify_list_id = $thold_data['notify_alert'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if (trim($alert_emails) != '' && $thold_data['restored_alert'] != 'on' && $thold_data['acknowledgment'] == '') {
$message = get_thold_restoral_text($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id']);
thold_mail($alert_emails, $alert_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file, $thold_data['graph_timespan']);
}
if ($notify_different) {
$notify_list_id = $thold_data['notify_warning'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if (trim($warning_emails) != '' && $thold_data['acknowledgment'] == '') {
thold_mail($warning_emails, $warning_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file);
}
}
thold_command_execution($thold_data, $h, false, false, true);
$save = [
'class' => 'alert2normal',
'thold_data' => $thold_data,
'subject' => $subject,
'host_data' => $h,
'breach_up' => $breach_up,
'breach_down' => $breach_down,
'warning_breach_up' => $warning_breach_up,
'warning_breach_down' => $warning_breach_down
];
api_plugin_hook_function('thold_action', $save);
if ($thold_snmp_traps && $thold_snmp_normal_traps) {
$thold_snmp_data = get_thold_snmp_data($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread']);
$thold_snmp_data['eventClass'] = 1;
$thold_snmp_data['eventSeverity'] = 1;
$thold_snmp_data['eventStatus'] = 1;
$thold_snmp_data['eventNotificationType'] = ST_NOTIFYRS + 1;
$thold_snmp_data['eventDeviceIp'] = gethostbyname($h['hostname']);
thold_snmptrap($thold_snmp_data, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
}
thold_log([
'type' => 0,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => '',
'current' => $thold_data['lastread'],
'status' => ST_NOTIFYRS,
'description' => $subject,
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
}
if ($maint_dev) {
thold_log([
'type' => 0,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => '',
'current' => $thold_data['lastread'],
'status' => ST_NOTIFYRS,
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
}
}
}
break;
case 1: // baseline
$bl_alert_prev = $thold_data['bl_alert'];
$bl_count_prev = $thold_data['bl_fail_count'];
$bl_fail_trigger = ($thold_data['bl_fail_trigger'] == '' ? $alert_bl_trigger : $thold_data['bl_fail_trigger']);
$thold_data['bl_alert'] = thold_check_baseline($thold_data['local_data_id'], $thold_data['data_source_name'], $thold_data['lastread'], $thold_data);
switch($thold_data['bl_alert']) {
case -2: // exception is active, Future Release 'todo'
break;
case -1: // reference value not available, Future Release 'todo'
break;
case 0: // all clear
// if we were at an alert status before
if ($bl_alert_prev != 0) {
thold_debug('Threshold Baseline check is Normal');
$subject = get_email_subject('NORMAL', false, $lastread, false, false, $thold_data);
if ($thold_data['bl_fail_count'] >= $bl_fail_trigger && $thold_data['restored_alert'] != 'on' && !$maint_dev) {
thold_debug('Threshold Baseline check returned to normal');
if ($syslog) {
logger($subject, $url, $syslog_priority, $syslog_facility);
}
$message = thold_mail_notification($alert_emails, $alert_bcc_emails, $subject, 'restoral', $thold_data['notify_alert'], $file_array, $thold_data, $h, $thold_data['graph_timespan']);
if ($notify_different) {
$notify_list_id = $thold_data['notify_warning'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if (trim($warning_emails) != '' && $thold_data['acknowledgment'] == '') {
thold_mail($warning_emails, $warning_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file);
}
}
thold_command_execution($thold_data, $h, false, false, true);
$save = [
'class' => 'blnormal',
'thold_data' => $thold_data,
'subject' => $subject,
'host_data' => $h
];
api_plugin_hook_function('thold_action', $save);
if ($thold_snmp_traps && $thold_snmp_normal_traps) {
$thold_snmp_data = get_thold_snmp_data($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread']);
$thold_snmp_data['eventClass'] = 1;
$thold_snmp_data['eventSeverity'] = 1;
$thold_snmp_data['eventStatus'] = 1;
$thold_snmp_data['eventDeviceIp'] = gethostbyname($h['hostname']);
$thold_snmp_data['eventNotificationType'] = ST_NOTIFYRS + 1;
thold_snmptrap($thold_snmp_data, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
}
if ($thold_data['reset_ack'] == 'on') {
db_execute_prepared('UPDATE thold_data
SET acknowledgment = ""
WHERE id = ?',
[$thold_data['id']]);
}
// Set the return to normal time
db_execute_prepared('UPDATE thold_data SET lastchanged = NOW() WHERE id = ?', [$thold_data['id']]);
thold_log([
'type' => 1,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => '',
'current' => $thold_data['lastread'],
'status' => ST_RESTORAL,
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
}
}
$thold_data['bl_fail_count'] = 0;
break;
case 1: // value is below calculated threshold
case 2: // value is above calculated threshold
if (!$maint_dev) {
$thold_data['bl_fail_count']++;
}
$breach_up = ($thold_data['bl_alert'] == STAT_HI);
$breach_down = ($thold_data['bl_alert'] == STAT_LO);
thold_debug('Threshold Baseline check breached');
// re-alert?
$ra_modulo = ($thold_data['repeat_alert'] == '' ? $realert : $thold_data['repeat_alert']);
$ra = ($thold_data['bl_fail_count'] > $bl_fail_trigger && !empty($ra_modulo) && ($thold_data['bl_fail_count'] % $ra_modulo) == 0);
if ($thold_data['bl_fail_count'] == $bl_fail_trigger || $ra) {
if (!$ra) {
db_execute_prepared('UPDATE thold_data
SET lastchanged = NOW()
WHERE id = ?',
[$thold_data['id']]);
if ($thold_data['persist_ack'] == 'on' || $thold_data['reset_ack'] == 'on') {
db_execute_prepared('UPDATE thold_data
SET acknowledgment = "on"
WHERE id = ?',
[$thold_data['id']]);
}
}
// If this is a realert and the operator has reset the ack, don't notify
if ($ra && $thold_data['reset_ack'] == 'on' && $thold_data['acknowledgment'] == '') {
$suspend_notify = true;
} else {
$suspend_notify = false;
}
$subject = get_email_subject('ALERT', false, $lastread, $ra, $breach_up, $thold_data);
if (!$suspend_notify && !$maint_dev) {
thold_debug('Alerting is necessary');
if ($syslog) {
logger($subject, $url, $syslog_priority, $syslog_facility);
}
$message = thold_mail_notification($alert_emails, $alert_bcc_emails, $subject, 'alert', $thold_data['notify_alert'], $file_array, $thold_data, $h, $thold_data['graph_timespan']);
if ($notify_different) {
$notify_list_id = $thold_data['notify_warning'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if (trim($warning_emails) != '' && $thold_data['acknowledgment'] == '') {
thold_mail($warning_emails, $warning_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file);
}
}
thold_command_execution($thold_data, $h, $breach_up, $breach_down, false);
$save = [
'class' => 'blalert',
'thold_data' => $thold_data,
'subject' => $subject,
'repeat_alert' => $ra,
'host_data' => $h,
'breach_up' => $breach_up,
'breach_down' => $breach_down
];
api_plugin_hook_function('thold_action', $save);
if ($thold_snmp_traps) {
$thold_snmp_data = get_thold_snmp_data($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread']);
$thold_snmp_data['eventClass'] = 3;
$thold_snmp_data['eventSeverity'] = $thold_data['snmp_event_severity'];
$thold_snmp_data['eventStatus'] = $thold_data['bl_alert'] + 1;
$thold_snmp_data['eventRealertStatus'] = ($ra ? ($breach_up ? 3 : 2) : 1);
$thold_snmp_data['eventNotificationType'] = ($ra ? ST_NOTIFYRA : ST_NOTIFYAL) + 1;
$thold_snmp_data['eventFailCount'] = $thold_data['bl_fail_count'];
$thold_snmp_data['eventFailDuration'] = $thold_data['bl_fail_count'] * $cacti_polling_interval;
$thold_snmp_data['eventFailCountTrigger'] = $bl_fail_trigger;
$thold_snmp_data['eventDeviceIp'] = gethostbyname($h['hostname']);
$thold_snmp_data['eventDescription'] = str_replace(
['<FAIL_COUNT>', '<FAIL_DURATION>'],
[$thold_snmp_data['eventFailCount'], $thold_snmp_data['eventFailDuration']],
$thold_snmp_data['eventDescription']
);
thold_snmptrap($thold_snmp_data, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
}
}
thold_log([
'type' => 1,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => ($breach_up ? $thold_data['bl_pct_up'] : $thold_data['bl_pct_down']),
'current' => $thold_data['lastread'],
'status' => ($ra ? ST_NOTIFYRA : ST_NOTIFYAL),
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
} else {
$subject = 'Thold Baseline Cache Log';
thold_log([
'type' => 1,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => ($breach_up ? $thold_data['bl_pct_up'] : $thold_data['bl_pct_down']),
'current' => $thold_data['lastread'],
'status' => ST_TRIGGERA,
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
}
break;
}
db_execute_prepared('UPDATE thold_data
SET thold_alert = 0,
thold_fail_count = 0,
bl_alert = ?,
bl_fail_count = ?,
thold_low = ?,
thold_hi = ?,
bl_thold_valid = ?
WHERE id = ?',
[
$thold_data['bl_alert'],
$thold_data['bl_fail_count'],
$thold_data['thold_low'],
$thold_data['thold_hi'],
$thold_data['bl_thold_valid'],
$thold_data['id']
]
);
break;
case 2: // time based
if (is_numeric($thold_data['lastread'])) {
$breach_up = ($thold_data['time_hi'] != '' && $thold_data['lastread'] > $thold_data['time_hi']);
$breach_down = ($thold_data['time_low'] != '' && $thold_data['lastread'] < $thold_data['time_low']);
$warning_breach_up = ($thold_data['time_warning_hi'] != '' && $thold_data['lastread'] > $thold_data['time_warning_hi']);
$warning_breach_down = ($thold_data['time_warning_low'] != '' && $thold_data['lastread'] < $thold_data['time_warning_low']);
} else {
$breach_up = $breach_down = $warning_breach_up = $warning_breach_down = false;
}
$step = db_fetch_cell_prepared('SELECT rrd_step
FROM data_template_data
WHERE local_data_id = ?',
[$thold_data['local_data_id']]);
// alerts
$trigger = $thold_data['time_fail_trigger'];
$time = time() - ($thold_data['time_fail_length'] * $step);
$failures = db_fetch_cell_prepared('SELECT COUNT(id)
FROM plugin_thold_log
WHERE threshold_id = ?
AND status IN (?, ?, ?)
AND time > ?',
[
$thold_data['id'],
ST_TRIGGERA,
ST_NOTIFYRA,
ST_NOTIFYAL,
$time
]
);
// warnings
$warning_trigger = $thold_data['time_warning_fail_trigger'];
$warning_time = time() - ($thold_data['time_warning_fail_length'] * $step);
$warning_failures = db_fetch_cell_prepared('SELECT COUNT(id)
FROM plugin_thold_log
WHERE threshold_id = ?
AND status IN (?, ?, ?)
AND time > ?',
[
$thold_data['id'],
ST_NOTIFYWA,
ST_TRIGGERW,
ST_NOTIFYRAW,
$warning_time
]
) + $failures;
if ($breach_up || $breach_down) {
$notify = false;
thold_debug('Threshold Time Based check breached HI:' . $thold_data['time_hi'] . ' LOW:' . $thold_data['time_low'] . ' VALUE:' . $thold_data['lastread']);
$thold_data['thold_alert'] = ($breach_up ? STAT_HI : STAT_LO);
$thold_data['thold_fail_count'] = $failures;
/**
* we should only re-alert X minutes after last email, not every 5 pollings, etc...
* re-alert?
*/
$realerttime = ($thold_data['repeat_alert'] - 1) * $step;
$lastemailtime = db_fetch_cell_prepared('SELECT time
FROM plugin_thold_log
WHERE threshold_id = ?
AND status IN (?, ?)
ORDER BY time DESC
LIMIT 1',
[$thold_data['id'], ST_NOTIFYRA, ST_NOTIFYAL]);
$ra = ($failures > $trigger && $thold_data['repeat_alert'] && !empty($lastemailtime) && ($lastemailtime + $realerttime <= time()));
if (!$maint_dev) {
$failures++;
}
thold_debug("Alert Time:'$time', Alert Trigger:'$trigger', Alert Failures:'$failures', RealertTime:'$realerttime', LastTime:'$lastemailtime', RA:'$ra', Diff:'" . ($realerttime + $lastemailtime) . "'<'" . time() . "'");
if (($failures == $trigger || $ra) && !$maint_dev) {
$notify = true;
}
if ($notify && !$ra) {
db_execute_prepared('UPDATE thold_data
SET lastchanged = NOW()
WHERE id = ?',
[$thold_data['id']]);
if ($thold_data['persist_ack'] == 'on' || $thold_data['reset_ack'] == 'on') {
db_execute_prepared('UPDATE thold_data
SET acknowledgment = "on"
WHERE id = ?',
[$thold_data['id']]);
}
}
// If this is a realert and the operator has reset the ack, don't notify
if ($notify && $ra && $thold_data['reset_ack'] == 'on' && $thold_data['acknowledgment'] == '') {
$suspend_notify = true;
} else {
$suspend_notify = false;
}
if ($notify) {
$etrigger = false;
} else {
$etrigger = true;
}
$subject = get_email_subject('ALERT', $etrigger, $lastread, $ra, $breach_up, $thold_data);
if ($notify) {
if (!$suspend_notify && !$maint_dev) {
thold_debug('Alerting is necessary');
if ($syslog) {
logger($subject, $url, $syslog_priority, $syslog_facility);
}
$message = thold_mail_notification($alert_emails, $alert_bcc_emails, $subject, 'alert', $thold_data['notify_alert'], $file_array, $thold_data, $h, $thold_data['graph_timespan']);
if ($notify_different) {
$notify_list_id = $thold_data['notify_warning'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if (trim($warning_emails) != '' && $thold_data['acknowledgment'] == '') {
thold_mail($warning_emails, $warning_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file);
}
}
thold_command_execution($thold_data, $h, $breach_up, $breach_down, false);
$save = [
'class' => 'alert',
'thold_data' => $thold_data,
'subject' => $subject,
'repeat_alert' => $ra,
'host_data' => $h,
'breach_up' => $breach_up,
'breach_down' => $breach_down,
'warning_breach_up' => $warning_breach_up,
'warning_breach_down' => $warning_breach_down
];
api_plugin_hook_function('thold_action', $save);
if ($thold_snmp_traps) {
$thold_snmp_data = get_thold_snmp_data($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread']);
$thold_snmp_data['eventClass'] = 3;
$thold_snmp_data['eventSeverity'] = $thold_data['snmp_event_severity'];
$thold_snmp_data['eventStatus'] = $thold_data['thold_alert'] + 1;
$thold_snmp_data['eventRealertStatus'] = ($ra ? ($breach_up ? 3 : 2) : 1);
$thold_snmp_data['eventNotificationType'] = ($ra ? ST_NOTIFYRA : ST_NOTIFYAL) + 1;
$thold_snmp_data['eventFailCount'] = $failures;
$thold_snmp_data['eventFailCountTrigger'] = $trigger;
$thold_snmp_data['eventDeviceIp'] = gethostbyname($h['hostname']);
$thold_snmp_data['eventDescription'] = str_replace('<FAIL_COUNT>', $thold_snmp_data['eventFailCount'], $thold_snmp_data['eventDescription']);
thold_snmptrap($thold_snmp_data, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
}
}
thold_log([
'type' => 2,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => ($breach_up ? $thold_data['time_hi'] : $thold_data['time_low']),
'current' => $thold_data['lastread'],
'status' => ($ra ? ST_NOTIFYRA : ST_NOTIFYAL),
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
} else {
thold_log([
'type' => 2,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => ($breach_up ? $thold_data['time_hi'] : $thold_data['time_low']),
'current' => $thold_data['lastread'],
'status' => ST_TRIGGERA,
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
}
db_execute_prepared('UPDATE thold_data
SET thold_alert = ?,
thold_fail_count = ?
WHERE id = ?',
[$thold_data['thold_alert'], $failures, $thold_data['id']]);
} elseif ($warning_breach_up || $warning_breach_down) {
$notify = false;
$thold_data['thold_alert'] = ($warning_breach_up ? STAT_HI : STAT_LO);
$thold_data['thold_warning_fail_count'] = $warning_failures;
/**
* we should only re-alert X minutes after last email, not every 5 pollings, etc...
* re-alert?
*/
$realerttime = ($thold_data['repeat_alert'] - 1) * $step;
$lastemailtime = db_fetch_cell_prepared('SELECT time
FROM plugin_thold_log
WHERE threshold_id = ?
AND status IN (?, ?)
ORDER BY time DESC
LIMIT 1',
[$thold_data['id'], ST_NOTIFYRAW, ST_NOTIFYWA]);
$ra = ($warning_failures > $warning_trigger && $thold_data['repeat_alert'] && !empty($lastemailtime) && ($lastemailtime + $realerttime <= time()));
if (!$maint_dev) {
$warning_failures++;
}
thold_debug("Warn Time:'$warning_time', Warn Trigger:'$warning_trigger', Warn Failures:'$warning_failures', RealertTime:'$realerttime', LastTime:'$lastemailtime', RA:'$ra', Diff:'" . ($realerttime + $lastemailtime) . "'<'" . time() . "'");
if (($warning_failures == $warning_trigger || $ra) && !$maint_dev) {
$notify = true;
}
if ($notify && !$ra) {
db_execute_prepared('UPDATE thold_data
SET lastchanged = NOW()
WHERE id = ?',
[$thold_data['id']]);
if ($thold_data['persist_ack'] == 'on' || $thold_data['reset_ack'] == 'on') {
db_execute_prepared('UPDATE thold_data
SET acknowledgment = "on"
WHERE id = ?',
[$thold_data['id']]);
}
}
// If this is a realert and the operator has reset the ack, don't notify
if ($notify && $ra && $thold_data['reset_ack'] == 'on' && $thold_data['acknowledgment'] == '') {
$suspend_notify = true;
} else {
$suspend_notify = false;
}
if ($notify) {
$trigger = false;
} else {
$trigger = true;
}
$ra = ($warning_failures > $warning_trigger) ? true : false;
$subject = get_email_subject('WARNING', $trigger, $lastread, $ra, $warning_breach_up, $thold_data);
if ($notify) {
if (!$suspend_notify && !$maint_dev) {
if ($syslog) {
logger($subject, $url, $syslog_priority, $syslog_facility);
}
if (trim($warning_emails) != '' && $thold_data['acknowledgment'] == '') {
$message = get_thold_warning_text($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id']);
$notify_list_id = $thold_data['notify_warning'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
thold_mail($warning_emails, $warning_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file, $thold_data['graph_timespan']);
}
$save = [
'class' => 'warn',
'thold_data' => $thold_data,
'subject' => $subject,
'repeat_alert' => $ra,
'host_data' => $h,
'breach_up' => $breach_up,
'breach_down' => $breach_down,
'warning_breach_up' => $warning_breach_up,
'warning_breach_down' => $warning_breach_down
];
api_plugin_hook_function('thold_action', $save);
if ($thold_snmp_traps && $thold_snmp_warning_traps) {
$thold_snmp_data = get_thold_snmp_data($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread']);
$thold_snmp_data['eventClass'] = 2;
$thold_snmp_data['eventSeverity'] = $thold_data['snmp_event_warning_severity'];
$thold_snmp_data['eventStatus'] = $thold_data['thold_alert'] + 1;
$thold_snmp_data['eventRealertStatus'] = ($ra ? ($warning_breach_up ? 3 : 2) : 1);
$thold_snmp_data['eventNotificationType'] = ($ra ? ST_NOTIFYRAW : ST_NOTIFYWA) + 1;
$thold_snmp_data['eventFailCount'] = $warning_failures;
$thold_snmp_data['eventFailCountTrigger'] = $warning_trigger;
$thold_snmp_data['eventDeviceIp'] = gethostbyname($h['hostname']);
$thold_snmp_data['eventDescription'] = str_replace('<FAIL_COUNT>', $thold_snmp_data['eventFailCount'], $thold_snmp_data['eventDescription']);
thold_snmptrap($thold_snmp_data, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
}
}
thold_log([
'type' => 2,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => ($warning_breach_up ? $thold_data['time_warning_hi'] : $thold_data['time_warning_low']),
'current' => $thold_data['lastread'],
'status' => ($ra ? ST_NOTIFYRAW : ST_NOTIFYWA),
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
} elseif ($alertstat != 0 && $warning_failures < $warning_trigger && $failures < $trigger) {
$subject = get_email_subject('ALERT > WARNING', false, $lastread, false, $warning_breach_up, $thold_data);
thold_log([
'type' => 2,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => ($warning_breach_up ? $thold_data['time_warning_hi'] : $thold_data['time_warning_low']),
'current' => $thold_data['lastread'],
'status' => ST_NOTIFYRAW,
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
} else {
thold_log([
'type' => 2,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => ($warning_breach_up ? $thold_data['time_warning_hi'] : $thold_data['time_warning_low']),
'current' => $thold_data['lastread'],
'status' => ST_TRIGGERW,
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $warning_emails,
'bcc_emails' => $warning_bcc_emails]
);
}
db_execute_prepared('UPDATE thold_data
SET thold_alert = ?,
thold_warning_fail_count = ?,
thold_fail_count = ?
WHERE id = ?',
[$thold_data['thold_alert'], $warning_failures, $failures, $thold_data['id']]);
} else {
thold_debug('Threshold Time Based check is Normal HI:' . $thold_data['time_hi'] . ' LOW:' . $thold_data['time_low'] . ' VALUE:' . $thold_data['lastread']);
$subject = get_email_subject('NORMAL', false, $lastread, false, false, $thold_data);
if ($alertstat != 0 && $warning_failures >= $warning_trigger && $thold_data['restored_alert'] != 'on') {
if (!$maint_dev) {
if ($syslog) {
logger($subject, $url, $syslog_priority, $syslog_facility);
}
$notify_list_id = $thold_data['notify_warning'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if (trim($warning_emails) != '' && $thold_data['restored_alert'] != 'on') {
$message = get_thold_restoral_text($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id']);
thold_mail($warning_emails, $warning_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file, $thold_data['graph_timespan']);
}
$save = [
'class' => 'warn2normal',
'thold_data' => $thold_data,
'subject' => $subject,
'host_data' => $h,
'breach_up' => $breach_up,
'breach_down' => $breach_down,
'warning_breach_up' => $warning_breach_up,
'warning_breach_down' => $warning_breach_down
];
api_plugin_hook_function('thold_action', $save);
if ($thold_snmp_traps && $thold_snmp_normal_traps) {
$thold_snmp_data = get_thold_snmp_data($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread']);
$thold_snmp_data['eventClass'] = 1;
$thold_snmp_data['eventSeverity'] = 1;
$thold_snmp_data['eventStatus'] = 1;
$thold_snmp_data['eventNotificationType'] = ST_NOTIFYRS + 1;
$thold_snmp_data['eventDeviceIp'] = gethostbyname($h['hostname']);
thold_snmptrap($thold_snmp_data, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
}
}
thold_log([
'type' => 2,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => '',
'current' => $thold_data['lastread'],
'status' => ST_NOTIFYRS,
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $warning_emails,
'bcc_emails' => $warning_bcc_emails]
);
db_execute_prepared('UPDATE thold_data
SET thold_alert = 0,
lastchanged = NOW(),
thold_warning_fail_count = ?,
thold_fail_count = ?
WHERE id = ?',
[$warning_failures, $failures, $thold_data['id']]);
if ($thold_data['reset_ack'] == 'on') {
db_execute_prepared('UPDATE thold_data
SET acknowledgment = ""
WHERE id = ?',
[$thold_data['id']]);
}
} elseif ($alertstat != 0 && $failures >= $trigger && $thold_data['restored_alert'] != 'on') {
$subject = get_email_subject('NORMAL', false, $lastread, false, false, $thold_data);
if (!$maint_dev) {
if ($syslog) {
logger($subject, $url, $syslog_priority, $syslog_facility);
}
$notify_list_id = $thold_data['notify_alert'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if (trim($alert_emails) != '' && $thold_data['restored_alert'] != 'on') {
$message = get_thold_restoral_text($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id']);
thold_mail($alert_emails, $alert_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file, $thold_data['graph_timespan']);
}
if ($notify_different) {
$notify_list_id = $thold_data['notify_warning'];
$format_file = thold_get_thold_notification_format_file($thold_data['id'], $notify_list_id);
if (trim($warning_emails) != '' && $thold_data['acknowledgment'] == '') {
thold_mail($warning_emails, $warning_bcc_emails, '', $subject, $message, $file_array, '', $notify_list_id, $h, $format_file);
}
}
thold_command_execution($thold_data, $h, false, false, true);
$save = [
'class' => 'alert2normal',
'thold_data' => $thold_data,
'subject' => $subject,
'host_data' => $h,
'breach_up' => $breach_up,
'breach_down' => $breach_down,
'warning_breach_up' => $warning_breach_up,
'warning_breach_down' => $warning_breach_down
];
api_plugin_hook_function('thold_action', $save);
if ($thold_snmp_traps && $thold_snmp_normal_traps) {
$thold_snmp_data = get_thold_snmp_data($thold_data['data_source_name'], $thold_data, $h, $thold_data['lastread']);
$thold_snmp_data['eventClass'] = 1;
$thold_snmp_data['eventSeverity'] = 1;
$thold_snmp_data['eventStatus'] = 1;
$thold_snmp_data['eventNotificationType'] = ST_NOTIFYRS + 1;
$thold_snmp_data['eventDeviceIp'] = gethostbyname($h['hostname']);
thold_snmptrap($thold_snmp_data, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
}
}
thold_log([
'type' => 2,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => '',
'current' => $thold_data['lastread'],
'status' => ST_NOTIFYRS,
'description' => ($maint_dev ? $subject . '. ' . __('Only logging, maint device', 'thold') : $subject),
'emails' => $alert_emails,
'bcc_emails' => $alert_bcc_emails]
);
db_execute_prepared('UPDATE thold_data
SET thold_alert = 0,
lastchanged = NOW(),
thold_warning_fail_count = ?,
thold_fail_count = ?
WHERE id = ?',
[$warning_failures, $failures, $thold_data['id']]);
if ($thold_data['reset_ack'] == 'on') {
db_execute_prepared('UPDATE thold_data
SET acknowledgment=""
WHERE id = ?',
[$thold_data['id']]);
}
} elseif ($alertstat != 0 && $thold_data['restored_alert'] != 'on') {
/*
* Neither the warning nor the alert failure counts reached
* their trigger, but the threshold was alerting last poll
* and is normal now: log the restoral so it is visible in
* the log, without re-sending a notification for a state
* transition that never crossed its own trigger.
*/
thold_log([
'type' => 2,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_data['id'],
'threshold_value' => '',
'current' => $thold_data['lastread'],
'status' => ST_NOTIFYRS,
'description' => $subject,
'emails' => '',
'bcc_emails' => '']
);
db_execute_prepared('UPDATE thold_data
SET thold_alert = 0,
lastchanged = NOW(),
thold_warning_fail_count = ?,
thold_fail_count = ?
WHERE id = ?',
[$warning_failures, $failures, $thold_data['id']]);
if ($thold_data['reset_ack'] == 'on') {
db_execute_prepared('UPDATE thold_data
SET acknowledgment = ""
WHERE id = ?',
[$thold_data['id']]);
}
} else {
db_execute_prepared('UPDATE thold_data
SET thold_fail_count = ?,
thold_warning_fail_count = ?
WHERE id = ?',
[$failures, $warning_failures, $thold_data['id']]);
}
}
break;
}
}
/**
* get_email_subject - composes the alert/warning/restoral notification subject line for a
* threshold, either from its built-in default wording or from its configured per-phase custom
* subject template (with `<PHASE>`/`<THRESHOLDVALUE>`/`<CURRENTVALUE>`/etc. tokens substituted).
*
* @param string $phase The notification phase: 'ALERT', 'WARNING', 'ALERT > WARNING', or 'NORMAL'.
* @param bool $trigger Whether this is a repeat/re-alert ('TRIGGER' wording) rather than the
* initial transition.
* @param mixed $lastread The threshold's current sample value.
* @param bool $ra Whether the threshold is still in a breached state (re-alert) rather than newly
* transitioning.
* @param bool $breach_up Whether the breach is above (true) or below (false) the bound.
* @param array &$thold_data The threshold row being notified about.
*
* @return string The composed subject line.
*/
function get_email_subject($phase, $trigger, $lastread, $ra, $breach_up, &$thold_data) {
$peralert = read_config_option('thold_enable_per_thold_body');
// setup base units
$baseu = db_fetch_cell_prepared('SELECT base_value
FROM graph_templates_graph
WHERE local_graph_id = ?',
[$thold_data['local_graph_id']]);
if ($thold_data['data_type'] == 2) {
$suffix = false;
} else {
$suffix = true;
}
$show_datasource = thold_datasource_required(thold_get_cached_name($thold_data), $thold_data['data_source_name']);
$show_units = ($thold_data['show_units'] ? true : false);
$units_suffix = $thold_data['units_suffix'];
$decimals = $thold_data['decimals'] >= 0 ? $thold_data['decimals'] : 2;
$hostname = db_fetch_cell_prepared('SELECT hostname
FROM host
WHERE id = ?',
[$thold_data['host_id']]);
$site = db_fetch_cell_prepared('SELECT s.name
FROM sites AS s
JOIN host AS h ON h.site_id = s.id
WHERE h.id = ?',
[$thold_data['host_id']]);
if (!$site) {
$site = __('Default', 'thold');
}
switch($thold_data['thold_type']) {
case '0': // High Low
if ($phase == 'ALERT') {
if ($breach_up) {
$value = $thold_data['thold_hi'];
} else {
$value = $thold_data['thold_low'];
}
} elseif ($phase == 'WARNING') {
if ($breach_up) {
$value = $thold_data['thold_warning_hi'];
} else {
$value = $thold_data['thold_warning_low'];
}
} elseif ($phase == 'ALERT > WARNING') {
if ($breach_up) {
$value = $thold_data['thold_warning_hi'];
} else {
$value = $thold_data['thold_warning_low'];
}
}
break;
case '1': // Baseline
if ($breach_up) {
$value = $thold_data['thold_hi'];
} else {
$value = $thold_data['thold_low'];
}
break;
case '2': // Time Based
if ($phase == 'ALERT') {
if ($breach_up) {
$value = $thold_data['time_hi'];
} else {
$value = $thold_data['time_low'];
}
} elseif ($phase == 'WARNING') {
if ($breach_up) {
$value = $thold_data['time_warning_hi'];
} else {
$value = $thold_data['time_warning_low'];
}
} elseif ($phase == 'ALERT > WARNING') {
if ($breach_up) {
$value = $thold_data['time_warning_hi'];
} else {
$value = $thold_data['time_warning_low'];
}
}
break;
}
if (!isset($value)) {
$value = $thold_data['lastread'];
}
if ($phase == 'NORMAL') {
if ($thold_data['email_subject_restoral'] == '' || $peralert == '') {
return 'NORMAL: ' . thold_get_cached_name($thold_data) .
($show_datasource ? ' [' . $thold_data['data_source_name'] . ']' : '') .
' restored to Normal Threshold with value ' .
thold_format_number($thold_data['lastread'], $decimals, $baseu, $suffix, $show_units, $units_suffix);
} else {
$find = [
'<PHASE>',
'<THOLDVALUE>',
'<CURRENTVALUE>',
'<THRESHOLDNAME>',
'<DSNAME>',
'<HOSTNAME>',
'<BREACHUP>',
'<REALERT>',
'<LOCATION>',
'<SITE>'
];
$replace = [
$phase,
$value,
$lastread,
thold_get_cached_name($thold_data),
$thold_data['data_source_name'],
$hostname,
($breach_up ? 'above' : 'below'),
($ra ? 'is still' : 'went'),
$thold_data['location'],
$site
];
return str_replace($find, $replace, $thold_data['email_subject_restoral']);
}
} elseif ($phase == 'WARNING' || $phase == 'ALERT > WARNING') {
if ($trigger) {
$phase = 'TRIGGER';
}
if ($thold_data['email_subject_warn'] == '' || $peralert == '') {
if ($phase == 'ALERT > WARNING') {
return $phase . ': ' . thold_get_cached_name($thold_data) .
($show_datasource ? ' [' . $thold_data['data_source_name'] . ']' : '') .
' changed to Warning Threshold with value ' .
thold_format_number($thold_data['lastread'], $decimals, $baseu, $suffix, $show_units, $units_suffix);
} else {
return $phase . ': ' . thold_get_cached_name($thold_data) .
($show_datasource ? ' [' . $thold_data['data_source_name'] . ']' : '') . ' ' .
($ra ? 'is still' : 'went') . ' ' .
($breach_up ? 'above' : 'below') .
($thold_data['thold_type'] == 1 ? ' Baseline Threshold of ' : ' Threshold of ') .
($breach_up ? thold_format_number($value, $decimals, $baseu, $suffix, $show_units, $units_suffix) :
thold_format_number($value, $decimals, $baseu, $suffix, $show_units, $units_suffix)) . ' with ' .
thold_format_number($lastread, $decimals, $baseu, $suffix, $show_units, $units_suffix);
}
} else {
$find = [
'<PHASE>',
'<THRESHOLDVALUE>',
'<CURRENTVALUE>',
'<THRESHOLDNAME>',
'<DSNAME>',
'<HOSTNAME>',
'<BREACHUP>',
'<REALERT>',
'<LOCATION>',
'<SITE>'
];
$replace = [
$phase,
$value,
$lastread,
thold_get_cached_name($thold_data),
$thold_data['data_source_name'],
$hostname,
($breach_up ? 'above' : 'below'),
($ra ? 'is still' : 'went'),
$thold_data['location'],
$site
];
return str_replace($find, $replace, $thold_data['email_subject_warn']);
}
} else {
if ($trigger) {
$phase = 'TRIGGER';
}
if ($thold_data['email_subject'] == '' || $peralert == '') {
return $phase . ': ' . thold_get_cached_name($thold_data) .
($show_datasource ? ' [' . $thold_data['data_source_name'] . ']' : '') . ' ' .
($ra ? 'is still' : 'went') . ' ' .
($breach_up ? 'above' : 'below') .
($thold_data['thold_type'] == 1 ? ' Baseline Threshold of ' : ' Threshold of ') .
($breach_up ? thold_format_number($value, $decimals, $baseu, $suffix, $show_units, $units_suffix) :
thold_format_number($value, $decimals, $baseu, $suffix, $show_units, $units_suffix)) . ' with ' .
thold_format_number($lastread, $decimals, $baseu, $suffix, $show_units, $units_suffix);
} else {
$find = [
'<PHASE>',
'<THRESHOLDVALUE>',
'<CURRENTVALUE>',
'<THRESHOLDNAME>',
'<DSNAME>',
'<HOSTNAME>',
'<BREACHUP>',
'<REALERT>',
'<LOCATION>',
'<SITE>'
];
$replace = [
$phase,
$value,
$lastread,
thold_get_cached_name($thold_data),
$thold_data['data_source_name'],
$hostname,
($breach_up ? 'above' : 'below'),
($ra ? 'is still' : 'went'),
$thold_data['location'],
$site
];
return str_replace($find, $replace, $thold_data['email_subject']);
}
}
}
/**
* get_thold_snmp_data - assembles the CACTI-THOLD-MIB varbind data (with `<TAG>` tokens in the
* event description expanded) used to send an SNMP trap for a threshold event.
*
* @param string $data_source_name The data source name the event pertains to.
* @param array $thold The threshold row the event is for.
* @param array $h The device row the threshold belongs to.
* @param mixed $currentval The threshold's current sample value.
*
* @global array $thold_types Threshold-type code => display label map, used to validate the
* threshold's configured type.
*
* @return array The assembled varbind name => value map for thold_snmptrap().
*/
function get_thold_snmp_data($data_source_name, $thold, $h, $currentval) {
global $thold_types;
// Do some replacement of variables
$thold_snmp_data = [
'eventDateRFC822' => date(DATE_RFC822),
'eventClass' => 3, // default - see CACTI-THOLD-MIB
'eventSeverity' => 3, // default - see CACTI-THOLD-MIB
'eventCategory' => ($thold['snmp_event_category'] ? $thold['snmp_event_category'] : ''),
'eventSource' => $thold['name_cache'],
'eventDescription' => '', // default - see CACTI-THOLD-MIB
'eventDevice' => $h['hostname'],
'eventDataSource' => $data_source_name,
'eventCurrentValue' => $currentval,
'eventHigh' => ($thold['thold_type'] == 0 ? $thold['thold_hi'] : ($thold['thold_type'] == 2 ? $thold['time_warning_hi'] : '')),
'eventLow' => ($thold['thold_type'] == 0 ? $thold['thold_low'] : ($thold['thold_type'] == 2 ? $thold['time_warning_low'] : '')),
'eventNotificationType' => 5, // default - see CACTI-THOLD-MIB
'eventStatus' => 3, // default - see CACTI-THOLD-MIB
'eventRealertStatus' => 1, // default - see CACTI-THOLD-MIB
'eventFailDuration' => 0, // default - see CACTI-THOLD-MIB
'eventFailCount' => 0, // default - see CACTI-THOLD-MIB
'eventFailDurationTrigger' => 0, // default - see CACTI-THOLD-MIB
'eventFailCountTrigger' => 0, // default - see CACTI-THOLD-MIB
];
if (isset($thold_types[$thold['thold_type']])) {
$thold_snmp_data['eventThresholdType'] = $thold['thold_type'];
} else {
$thold_snmp_data['eventThresholdType'] = 1;
}
// substitute snmp query data
$snmp_event_description = thold_expand_string($thold, $thold['snmp_event_description']);
$snmp_event_description = thold_str_replace('<THRESHOLDNAME>', $thold_snmp_data['eventSource'], $snmp_event_description);
$snmp_event_description = thold_str_replace('<HOSTNAME>', $thold_snmp_data['eventDevice'], $snmp_event_description);
$snmp_event_description = thold_str_replace('<TEMPLATE_ID>', ($thold['thold_template_id'] ? $thold['thold_template_id'] : 'none'), $snmp_event_description);
$snmp_event_description = thold_str_replace('<TEMPLATE_NAME>', (isset($thold['name_cache']) ? $thold['name_cache'] : 'none'), $snmp_event_description);
$snmp_event_description = thold_str_replace('<THR_TYPE>', $thold_snmp_data['eventThresholdType'], $snmp_event_description);
$snmp_event_description = thold_str_replace('<DS_NAME>', $thold_snmp_data['eventDataSource'], $snmp_event_description);
$snmp_event_description = thold_str_replace('<HI>', $thold_snmp_data['eventHigh'], $snmp_event_description);
$snmp_event_description = thold_str_replace('<LOW>', $thold_snmp_data['eventLow'], $snmp_event_description);
$snmp_event_description = thold_str_replace('<EVENT_CATEGORY>', $thold_snmp_data['eventCategory'], $snmp_event_description);
$thold_snmp_data['eventDescription'] = $snmp_event_description;
return $thold_snmp_data;
}
/**
* thold_expand_string - resolves a threshold's data-source/graph/host/query/custom-data tokens
* (`|graph_title|`, `|data_source_name|`, `|data_source_description|`, `|host_*|`, `|query_*|`,
* `|custom_*|`) in a string, falling back to the threshold template's or the default suggested
* name when given an empty string.
*
* @param array $thold_data The threshold row providing the substitution context.
* @param string $string The raw string containing tokens to resolve.
* @param bool $shell When true, substituted values are shell-escaped and deferred via
* thold_defer_placeholder() until every substitution phase has run.
* @param array|null &$deferred Shared placeholder => shell-escaped-value map; pass null to have
* this call resolve its own placeholders before returning.
*
* @global array $config Cacti's global configuration array.
*
* @return string The string with all resolvable tokens substituted.
*/
function thold_expand_string($thold_data, $string, $shell = false, ?array &$deferred = null) {
global $config;
include_once($config['library_path'] . '/variables.php');
$defer_internally = $deferred === null;
if ($defer_internally) {
$deferred = [];
}
// Values substituted below (data source names/descriptions, graph
// titles, and host/data-query fields sourced from the polled device
// itself) are not admin-controlled, so in $shell mode they must be
// quoted before landing on a command line, same as
// thold_replace_threshold_tags()'s $q(). The quoted value is deferred
// behind an opaque placeholder rather than inserted immediately, so a
// later phase's token scan (or a caller chaining another substitution
// pass afterward) can never mistake it for real token syntax and
// re-substitute inside its quoting.
$q = function ($value) use ($shell, &$deferred) {
if (!$shell) {
return $value;
}
$placeholder = thold_defer_placeholder();
$deferred[$placeholder] = cacti_escapeshellarg((string) $value);
return $placeholder;
};
$str = $string;
// Handle the blank string case
if ($str == '') {
if (isset($thold_data['thold_template_id']) && $thold_data['thold_template_id'] > 0) {
$str = db_fetch_cell_prepared('SELECT suggested_name
FROM thold_template
WHERE id = ?',
[$thold_data['thold_template_id']]);
if ($str == '') {
$str = '|data_source_description| [|data_source_name|]';
}
} elseif (isset($thold_data['data_source_name']) && strlen($thold_data['data_source_name']) > 0) {
$str = thold_get_default_suggested_name(['data_source_name' => $thold_data['data_source_name']], 0);
}
}
// Do core replacements
if (cacti_sizeof($thold_data) && isset($thold_data['local_graph_id']) && isset($thold_data['local_data_id'])) {
$lg = db_fetch_row_prepared('SELECT *
FROM graph_local
WHERE id = ?',
[$thold_data['local_graph_id']]);
if (cacti_sizeof($lg)) {
// Handle speed queries replacements first
if (strpos($str, '|query_ifHighSpeed|') !== false) {
$value = thold_substitute_snmp_query_data('|query_ifHighSpeed|', $lg['host_id'], $lg['snmp_query_id'], $lg['snmp_index'], read_config_option('max_data_query_field_length'));
// if we are trying to replace 10GE of some odd data
if (!is_numeric($value) || $value == 0) {
$value = read_config_option('thold_empty_if_speed_default');
}
$str = str_replace('|query_ifHighSpeed|', $q($value), $str);
} elseif (strpos($str, '|query_ifSpeed|') !== false) {
$value = thold_substitute_snmp_query_data('|query_ifSpeed|', $lg['host_id'], $lg['snmp_query_id'], $lg['snmp_index'], read_config_option('max_data_query_field_length'));
if (!is_numeric($value) || $value == 0) {
$value = read_config_option('thold_empty_if_speed_default');
}
$str = str_replace('|query_ifSpeed|', $q($value), $str);
}
if ($shell) {
// expand_title() (Cacti core) substitutes |host_*|/|query_*|
// tokens with raw values -- hostname, SNMP community/
// password, sysDescr/sysContact/sysLocation, polled data
// query fields, etc -- with no shell escaping, and any of
// them can be set by the polled device itself. Resolve each
// token in isolation through the same core helpers so the
// substituted value matches what expand_title() would have
// produced, but escape it before it lands in the command.
preg_match_all('/\|(?:host|query)_[A-Za-z0-9_]+\|/', $str, $host_query_tokens);
$host_query_replacements = [];
foreach (array_unique($host_query_tokens[0]) as $token) {
if ($lg['snmp_query_id'] != '0' && $lg['snmp_index'] != '') {
$resolved = substitute_snmp_query_data(
null_out_substitutions(substitute_host_data($token, '|', '|', $lg['host_id'])),
$lg['host_id'], $lg['snmp_query_id'], $lg['snmp_index'],
intval(read_config_option('max_data_query_field_length'))
);
} else {
$resolved = null_out_substitutions(substitute_host_data($token, '|', '|', $lg['host_id']));
}
if ($resolved !== $token) {
$host_query_replacements[$token] = $q($resolved);
}
}
// A single strtr() pass replaces against the original string only and
// never re-scans inserted values, so a resolved host/query value that
// happens to contain another token's literal text can't be substituted
// a second time outside of its own quoting.
$str = strtr($str, $host_query_replacements);
$str = null_out_substitutions($str);
} else {
$str = expand_title($lg['host_id'], $lg['snmp_query_id'], $lg['snmp_index'], $str);
}
$str = thold_substitute_custom_data($str, '|', '|', $thold_data['local_data_id'], $shell, $deferred);
$data = [
'str' => $str,
'thold_data' => $thold_data,
'local_graph' => $lg
];
$data = api_plugin_hook_function('thold_substitute_custom_data', $data);
if (isset($data['str'])) {
$str = $data['str'];
}
if (strpos($str, '|query_') !== false) {
cacti_log("WARNING: Expression Replacement for '$str' in THold '" . $thold_data['name'] . "' Failed, A Reindex may be required!");
}
}
if (strpos($str, '|host_') !== false && !empty($device_id)) {
$str = thold_substitute_host_data($str, '|', '|', $device_id, $shell, $deferred);
}
// Replace |graph_title|, |data_source_description|, and |data_source_name|
// in a single strtr() pass so a resolved value containing another
// token's literal text can't be substituted a second time outside of
// its own quoting.
$direct_replacements = [];
if (strpos($str, '|graph_title|') !== false) {
$direct_replacements['|graph_title|'] = $q(get_graph_title($thold_data['local_graph_id']));
}
if (strpos($str, '|data_source_description|') !== false) {
$data_source_desc = db_fetch_cell_prepared('SELECT name_cache
FROM data_template_data
WHERE local_data_id = ?',
[$thold_data['local_data_id']]);
$direct_replacements['|data_source_description|'] = $q($data_source_desc);
}
if (strpos($str, '|data_source_name|') !== false) {
$direct_replacements['|data_source_name|'] = $q($thold_data['data_source_name']);
}
$str = strtr($str, $direct_replacements);
}
if ($defer_internally) {
$str = strtr($str, $deferred);
}
return trim($str);
}
/**
* thold_command_execution - runs (or, when the notification queue is enabled, enqueues) the
* threshold's configured high/low/normal trigger command for the breach direction that occurred,
* expanding its token placeholders and environment variables first.
*
* @param array &$thold_data The threshold row, including its trigger_cmd_high/low/norm commands.
* @param array &$h The device row the threshold belongs to.
* @param bool $breach_up Whether the threshold breached above its high bound.
* @param bool $breach_down Whether the threshold breached below its low bound.
* @param bool $breach_norm Whether the threshold returned to normal.
*
* @return void
*/
function thold_command_execution(&$thold_data, &$h, $breach_up, $breach_down, $breach_norm = false) {
if (read_config_option('thold_enable_scripts') == 'on') {
$output = [];
$return = 0;
$command_executed = false;
$data_source_name = $thold_data['data_source_name'];
$queue = read_config_option('thold_notification_queue');
if ($breach_up && $thold_data['trigger_cmd_high'] != '') {
$deferred = [];
$cmd = thold_expand_string($thold_data, $thold_data['trigger_cmd_high'], true, $deferred);
$cmd = thold_replace_threshold_tags($cmd, $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id'], $data_source_name, true, $deferred);
// Both calls above deferred their quoted values behind opaque
// placeholders instead of inserting them immediately, so neither
// one's value could be re-scanned (and have its quoting broken) by
// the other's token substitution. Resolve every placeholder now
// that no further token scanning will happen.
$cmd = strtr($cmd, $deferred);
$environment = thold_set_environ($thold_data['trigger_cmd_high'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id'], $data_source_name);
if ($queue == 'on') {
$data = [
'command' => $cmd,
'data' => $thold_data,
'environment' => $environment
];
thold_notification_add('thold_cmd', $data, 'id', 0, $h);
} else {
exec($cmd, $output, $return); // nosemgrep: php.lang.security.exec-use.exec-use -- admin-configured alert command; $cmd is built from thold_replace_threshold_tags + thold_expand_string with cacti_escapeshellarg protection
}
$command_executed = true;
} elseif ($breach_down && $thold_data['trigger_cmd_low'] != '') {
$deferred = [];
$cmd = thold_expand_string($thold_data, $thold_data['trigger_cmd_low'], true, $deferred);
$cmd = thold_replace_threshold_tags($cmd, $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id'], $data_source_name, true, $deferred);
$cmd = strtr($cmd, $deferred);
$environment = thold_set_environ($thold_data['trigger_cmd_high'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id'], $data_source_name);
if ($queue == 'on') {
$data = [
'command' => $cmd,
'data' => $thold_data,
'environment' => $environment
];
thold_notification_add('thold_cmd', $data, 'id', 0, $h);
} else {
exec($cmd, $output, $return); // nosemgrep: php.lang.security.exec-use.exec-use -- admin-configured alert command; $cmd is built from thold_replace_threshold_tags + thold_expand_string with cacti_escapeshellarg protection
}
$command_executed = true;
} elseif ($breach_norm && $thold_data['trigger_cmd_norm'] != '') {
$deferred = [];
$cmd = thold_expand_string($thold_data, $thold_data['trigger_cmd_norm'], true, $deferred);
$cmd = thold_replace_threshold_tags($cmd, $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id'], $data_source_name, true, $deferred);
$cmd = strtr($cmd, $deferred);
$environment = thold_set_environ($thold_data['trigger_cmd_high'], $thold_data, $h, $thold_data['lastread'], $thold_data['local_graph_id'], $data_source_name);
if ($queue == 'on') {
$data = [
'command' => $cmd,
'data' => $thold_data,
'environment' => $environment
];
thold_notification_add('thold_cmd', $data, 'id', 0, $h);
} else {
exec($cmd, $output, $return); // nosemgrep: php.lang.security.exec-use.exec-use -- admin-configured alert command; $cmd is built from thold_replace_threshold_tags + thold_expand_string with cacti_escapeshellarg protection
}
$command_executed = true;
}
if ($queue == '' && $command_executed) {
thold_process_command_output($output, $return, 'thold_cmd', $thold_data, $cmd);
}
}
}
/**
* thold_process_command_output - logs the result of running a threshold trigger command, a
* device-down command, or a device-up command, at a verbosity matching whether it exited
* successfully.
*
* @param array &$output The command's captured output lines.
* @param int $return The command's exit status.
* @param string $topic Which command type this is: 'thold_cmd', 'thold_dhost_cmd', or
* 'thold_uhost_cmd'.
* @param array &$data The threshold or device row the command was run for (used for its id in
* the log line).
* @param string $command The command string, used only in the device up/down log lines.
*
* @return void
*/
function thold_process_command_output(&$output, $return, $topic = 'thold_cmd', &$data = [], $command = '') {
if ($topic == 'thold_cmd') {
if (cacti_sizeof($output)) {
if ($return > 0) {
cacti_log('WARNING: Threshold command execution for TH[' . $data['id'] . '] returned ' . $return . ', with output ' . implode(', ', $output), false, 'THOLD');
} else {
cacti_log('NOTE: Threshold command execution for TH[' . $data['id'] . '] returned ' . $return . ', with output ' . implode(', ', $output), false, 'THOLD');
}
} else {
if ($return > 0) {
cacti_log('WARNING: Threshold command execution for TH[' . $data['id'] . '] returned ' . $return . ', with no output.', false, 'THOLD');
} else {
cacti_log('NOTE: Threshold command execution for TH[' . $data['id'] . '] returned ' . $return . ', with no output.', false, 'THOLD');
}
}
} elseif ($topic == 'thold_dhost_cmd') {
cacti_log('Device Down Command for Device[' . $data['id'] . '] Command[' . $command . '] ExitStatus[' . $return . '] Output[' . implode(' ', $output) . ']', false, 'THOLD');
} elseif ($topic == 'thold_uhost_cmd') {
cacti_log('Device Up Command for Device[' . $data['id'] . '] Command[' . $command . '] ExitStatus[' . $return . '] Output[' . implode(' ', $output) . ']', false, 'THOLD');
}
}
/**
* thold_putenv - sets a process environment variable for a trigger command, or queues it for
* later application when the notification queue is enabled.
*
* @param string $name_value_pair The `NAME=value` pair to set/queue.
* @param bool $reset When true, clears the queued environment variables before adding this one.
*
* @return array The current queued environment variable list.
*/
function thold_putenv($name_value_pair, $reset = false) {
static $env_queue = [];
$queue = read_config_option('thold_notification_queue');
if ($reset == true) {
$env_queue = [];
}
if ($queue == 'on') {
$env_queue[] = $name_value_pair;
} else {
putenv($name_value_pair);
}
return $env_queue;
}
/**
* thold_set_environ - populates the THOLD_* environment variables (id, data/graph ids, device
* info, current value, threshold name/type, notes, hi/lo/time bounds, etc.) a trigger command can
* reference, via thold_putenv().
*
* @param string $text The threshold's notes text, expanded via thold_replace_threshold_tags().
* @param array &$thold The threshold row providing most of the environment values.
* @param array &$h The device row the threshold belongs to.
* @param mixed $currentval The threshold's current sample value.
* @param int $local_graph_id The local graph id the threshold is attached to.
* @param string $data_source_name The data source name the threshold monitors.
*
* @global array $thold_types Threshold-type code => display label map.
*
* @return void
*/
function thold_set_environ($text, &$thold, &$h, $currentval, $local_graph_id, $data_source_name) {
global $thold_types;
$httpurl = read_config_option('base_url');
$queue = read_config_option('thold_notification_queue');
// Do some replacement of variables
thold_putenv('THOLD_ID=' . $thold['id'], true);
thold_putenv('THOLD_DATA_ID=' . $thold['local_data_id']);
thold_putenv('THOLD_GRAPH_ID=' . $thold['local_graph_id']);
thold_putenv('THOLD_DESCRIPTION=' . $h['description']);
thold_putenv('THOLD_HOSTNAME=' . $h['hostname']);
thold_putenv('THOLD_GRAPHID=' . $local_graph_id);
thold_putenv('THOLD_CURRENTVALUE=' . $currentval);
thold_putenv('THOLD_THRESHOLDNAME=' . $thold['name_cache']);
thold_putenv('THOLD_DSNAME=' . $data_source_name);
thold_putenv('THOLD_TIMEINSTATE=' . get_timeinstate($h));
thold_putenv('THOLD_HOST_STATUS=' . $h['status']);
thold_putenv('THOLD_HOST_FAIL_DATE=' . $h['status_fail_date']);
thold_putenv('THOLD_HOST_REC_DATE=' . $h['status_rec_date']);
thold_putenv('THOLD_HOST_LAST_ERROR=' . $h['status_last_error']);
if (isset($thold_types[$thold['thold_type']])) {
thold_putenv('THOLD_THOLDTYPE=' . $thold_types[$thold['thold_type']]);
} else {
thold_putenv('THOLD_THOLDTYPE=');
}
if ($thold['notes'] != '') {
$notes = thold_replace_threshold_tags($thold['notes'], $thold, $h, $currentval, $local_graph_id, $data_source_name);
thold_putenv('THOLD_NOTES=' . $notes);
} else {
thold_putenv('THOLD_NOTES=' . '');
}
if ($thold['external_id'] != '') {
thold_putenv('THOLD_EXTERNAL_ID=' . $thold['external_id']);
}
thold_putenv('THOLD_DEVICENOTE=' . $thold['dnotes']);
if ($thold['thold_type'] == 0) {
thold_putenv('THOLD_HI=' . $thold['thold_hi']);
thold_putenv('THOLD_LOW=' . $thold['thold_low']);
thold_putenv('THOLD_TRIGGER=' . $thold['thold_fail_trigger']);
thold_putenv('THOLD_DURATION=' . '');
} elseif ($thold['thold_type'] == 2) {
thold_putenv('THOLD_HI=' . $thold['time_hi']);
thold_putenv('THOLD_LOW=' . $thold['time_low']);
thold_putenv('THOLD_TRIGGER=' . $thold['time_fail_trigger']);
thold_putenv('THOLD_DURATION=' . plugin_thold_duration_convert($thold['local_data_id'], $thold['time_fail_length'], 'time'));
} else {
thold_putenv('THOLD_HI=' . '');
thold_putenv('THOLD_LOW=' . '');
thold_putenv('THOLD_TRIGGER=' . '');
thold_putenv('THOLD_DURATION=' . '');
}
thold_putenv('THOLD_TIME=' . time());
thold_putenv('THOLD_DATE=' . date(CACTI_DATE_TIME_FORMAT));
thold_putenv('THOLD_DATE_RFC822=' . date(DATE_RFC822));
$environment = thold_putenv('THOLD_URL=' . html_escape("$httpurl/graph.php?local_graph_id=$local_graph_id"));
return $environment;
}
/**
* thold_replace_threshold_tags - resolves the general `<TAG>` placeholders (description,
* hostname, location, site, current value, threshold name/notes/bounds, URL, date, etc.) used in
* threshold notification text, notes, and trigger commands.
*
* @param string $text The raw text containing `<TAG>` placeholders to resolve.
* @param array &$thold The threshold row providing most of the replacement values.
* @param array &$h The device row the threshold belongs to.
* @param mixed $currentval The threshold's current sample value.
* @param int $local_graph_id The local graph id the threshold is attached to.
* @param string $data_source_name The data source name the threshold monitors.
* @param bool $shell When true, substituted values are shell-escaped and deferred via
* thold_defer_placeholder() until every substitution phase has run.
* @param array|null &$deferred Shared placeholder => shell-escaped-value map; pass null to have
* this call resolve its own placeholders before returning.
*
* @global array $thold_types Threshold-type code => display label map.
*
* @return string The text with all resolvable tags substituted.
*/
function thold_replace_threshold_tags($text, &$thold, &$h, $currentval, $local_graph_id, $data_source_name, $shell = false, ?array &$deferred = null) {
global $thold_types;
$defer_internally = $deferred === null;
if ($defer_internally) {
$deferred = [];
}
if (substr(read_config_option('base_url'), 0, 4) != 'http') {
if (read_config_option('force_https') == 'on') {
$prefix = 'https://';
} else {
$prefix = 'http://';
}
set_config_option('base_url', $prefix . read_config_option('base_url'));
}
$httpurl = read_config_option('base_url', true);
$site = db_fetch_cell_prepared('SELECT name
FROM sites
WHERE id = ?',
[$h['site_id']]);
if (!$site) {
$site = __('Default', 'thold');
}
// Device and threshold free-text values are admin/user editable. When $text
// is a trigger command template ($shell), quote them so they cannot
// terminate the command and start another. The quoted value is deferred
// behind an opaque placeholder rather than inserted immediately, so a
// caller chaining another substitution pass afterward (thold_expand_string())
// can never mistake it for real token syntax and re-substitute inside its
// quoting.
$q = function ($value) use ($shell, &$deferred) {
if (!$shell) {
return $value;
}
$placeholder = thold_defer_placeholder();
$deferred[$placeholder] = cacti_escapeshellarg((string) $value);
return $placeholder;
};
// Do some replacement of variables. Every tag/value pair is collected up
// front and substituted in a single strtr() pass: strtr() replaces against
// the original text only and never re-scans inserted values, so a value
// that happens to contain another tag's literal placeholder (for example
// a <DESCRIPTION> of "<HOSTNAME>") can't be substituted a second time
// outside of $q()'s quoting.
$replacements = [
'<DESCRIPTION>' => $q($h['description']),
'<HOSTNAME>' => $q($h['hostname']),
'<LOCATION>' => $q($h['location']),
'<SITE>' => $q($site),
'<GRAPHID>' => $local_graph_id,
'<THOLD_ID>' => $thold['id'],
'<CURRENTVALUE>' => $q($currentval),
'<THRESHOLDNAME>' => $q($thold['name_cache']),
'<DSNAME>' => $q($data_source_name),
'<NOTES>' => $q($thold['notes']),
'<DNOTES>' => $q($thold['dnotes']),
'<DEVICENOTE>' => $q($thold['dnotes']),
'<EXTERNALID>' => $q($thold['external_id']),
'<TIME>' => time(),
'<DATE>' => date(CACTI_DATE_TIME_FORMAT),
'<DATE_RFC822>' => date(DATE_RFC822),
'<URL>' => $q("<a href='" . html_escape("$httpurl/graph.php?local_graph_id=$local_graph_id") . "'>" . __('Link to Graph in Cacti', 'thold') . '</a>'),
];
if (isset($thold_types[$thold['thold_type']])) {
$replacements['<THOLDTYPE>'] = $thold_types[$thold['thold_type']];
}
if ($thold['thold_type'] == 0) {
$replacements['<HI>'] = $thold['thold_hi'];
$replacements['<LOW>'] = $thold['thold_low'];
$replacements['<TRIGGER>'] = $thold['thold_fail_trigger'];
$replacements['<DURATION>'] = '';
} elseif ($thold['thold_type'] == 2) {
$replacements['<HI>'] = $thold['time_hi'];
$replacements['<LOW>'] = $thold['time_low'];
$replacements['<TRIGGER>'] = $thold['time_fail_trigger'];
$replacements['<DURATION>'] = plugin_thold_duration_convert($thold['local_data_id'], $thold['time_fail_length'], 'time');
} else {
$replacements['<HI>'] = '';
$replacements['<LOW>'] = '';
$replacements['<TRIGGER>'] = '';
$replacements['<DURATION>'] = '';
}
$text = strtr($text, array_map(static function ($value) {
return (string) ($value ?? '');
}, $replacements));
$data = [
'thold_data' => $thold,
'text' => $text
];
$data = api_plugin_hook_function('thold_replacement_text', $data);
if (isset($data['text'])) {
$text = $data['text'];
}
if ($defer_internally) {
$text = strtr($text, $deferred);
}
return $text;
}
/**
* get_thold_alert_text - builds the alert notification body, using the threshold's own custom
* body (when per-threshold bodies are enabled) or the global default, with `<TAG>` placeholders
* resolved via thold_replace_threshold_tags().
*
* @param string $data_source_name The data source name the threshold monitors.
* @param array $thold The threshold row.
* @param array $h The device row the threshold belongs to.
* @param mixed $currentval The threshold's current sample value.
* @param int $local_graph_id The local graph id the threshold is attached to.
*
* @return string The resolved alert notification body.
*/
function get_thold_alert_text($data_source_name, $thold, $h, $currentval, $local_graph_id) {
$alert_text = read_config_option('thold_alert_text');
$httpurl = read_config_option('base_url');
$peralert = read_config_option('thold_enable_per_thold_body');
if ($peralert == 'on') {
$alert_text = $thold['email_body'];
}
// make sure the alert text has been set
if ($alert_text == '') {
$alert_text = __('<html><body>An alert has been issued that requires your attention.<br><br><b>Device</b>: <DESCRIPTION> (<HOSTNAME>)<br><b>URL</b>: <URL><br><b>Message</b>: <SUBJECT><br><br><GRAPH></body></html>', 'thold');
}
if ($thold['notes'] != '') {
$notes = thold_replace_threshold_tags($thold['notes'], $thold, $h, $currentval, $local_graph_id, $data_source_name);
$alert_text = thold_str_replace('<NOTES>', $notes, $alert_text);
}
$alert_text = thold_replace_threshold_tags($alert_text, $thold, $h, $currentval, $local_graph_id, $data_source_name);
return $alert_text;
}
/**
* get_thold_warning_text - builds the warning notification body, using the threshold's own custom
* body (when per-threshold bodies are enabled) or the global default, with `<TAG>` placeholders
* resolved via thold_replace_threshold_tags().
*
* @param string $data_source_name The data source name the threshold monitors.
* @param array $thold The threshold row.
* @param array $h The device row the threshold belongs to.
* @param mixed $currentval The threshold's current sample value.
* @param int $local_graph_id The local graph id the threshold is attached to.
*
* @return string The resolved warning notification body.
*/
function get_thold_warning_text($data_source_name, $thold, $h, $currentval, $local_graph_id) {
$warning_text = read_config_option('thold_warning_text');
$httpurl = read_config_option('base_url');
$peralert = read_config_option('thold_enable_per_thold_body');
if ($peralert == 'on') {
$warning_text = $thold['email_body_warn'];
}
// make sure the warning text has been set
if ($warning_text == '') {
$warning_text = __('<html><body>A warning has been issued that requires your attention.<br><br><b>Device</b>: <DESCRIPTION> (<HOSTNAME>)<br><b>URL</b>: <URL><br><b>Message</b>: <SUBJECT><br><br><GRAPH></body></html>', 'thold');
}
if ($thold['notes'] != '') {
$notes = thold_replace_threshold_tags($thold['notes'], $thold, $h, $currentval, $local_graph_id, $data_source_name);
$warning_text = thold_str_replace('<NOTES>', $notes, $warning_text);
}
$warning_text = thold_replace_threshold_tags($warning_text, $thold, $h, $currentval, $local_graph_id, $data_source_name);
return $warning_text;
}
/**
* get_thold_restoral_text - builds the restoral notification body, using the threshold's own
* custom body (when per-threshold bodies are enabled) or the global default, with `<TAG>`
* placeholders resolved via thold_replace_threshold_tags().
*
* @param string $data_source_name The data source name the threshold monitors.
* @param array $thold The threshold row.
* @param array $h The device row the threshold belongs to.
* @param mixed $currentval The threshold's current sample value.
* @param int $local_graph_id The local graph id the threshold is attached to.
*
* @return string The resolved restoral notification body.
*/
function get_thold_restoral_text($data_source_name, $thold, $h, $currentval, $local_graph_id) {
$restoral_text = read_config_option('thold_restoral_text');
$httpurl = read_config_option('base_url');
$peralert = read_config_option('thold_enable_per_thold_body');
if ($peralert == 'on') {
$restoral_text = $thold['email_body_restoral'];
}
// make sure the alert text has been set
if ($restoral_text == '') {
$restoral_text = __('<html><body>A Threshold has returned to normal status. <br><br><strong>Device</strong>: <DESCRIPTION> (<HOSTNAME>)<br><strong>URL</strong>: <URL><br><strong>Message</strong>: <SUBJECT><br><br><GRAPH></body></html>', 'thold');
}
if ($thold['notes'] != '') {
$notes = thold_replace_threshold_tags($thold['notes'], $thold, $h, $currentval, $local_graph_id, $data_source_name);
$restoral_text = thold_str_replace('<NOTES>', $notes, $restoral_text);
}
$restoral_text = thold_replace_threshold_tags($restoral_text, $thold, $h, $currentval, $local_graph_id, $data_source_name);
return $restoral_text;
}
/**
* thold_modify_values_by_cdef - applies a graph item's CDEF (explicitly configured on the
* threshold, or auto-detected from its graph item) to the threshold's hi/lo (or time-based hi/lo)
* bounds in place, so they compare consistently against an already-CDEF-transformed current value.
*
* @param array &$thold_data The threshold row, modified in place with CDEF-transformed bounds.
*
* @return void
*/
function thold_modify_values_by_cdef(&$thold_data) {
$cdef = false;
if ($thold_data['data_type'] == 1) {
if (!empty($thold_data['cdef'])) {
// Use the CDEF explicitly configured on the threshold
$cdef = $thold_data['cdef'];
} else {
// Auto-detect a CDEF from the graph item for this data source
$cdef = db_fetch_cell_prepared('SELECT MAX(cdef_id)
FROM graph_templates_item AS gti
INNER JOIN data_template_rrd AS dtr
ON gti.task_item_id = dtr.id
WHERE local_graph_id = ?
AND dtr.id = ?
AND gti.graph_type_id IN (4, 5, 6, 7, 8, 20)
AND dtr.data_source_name = ?',
[$thold_data['local_graph_id'], $thold_data['data_template_rrd_id'], $thold_data['data_source_name']]);
}
}
if ($cdef !== false && $cdef > 0) {
// Note: lastread is already CDEF-transformed by thold_poller_output before
// being stored, so we must not apply the CDEF to it again here. We only
// transform the threshold values so the comparison is consistent.
if ($thold_data['thold_type'] == 0) {
$thold_data['thold_hi'] = thold_build_cdef($cdef, $thold_data['thold_hi'], $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
$thold_data['thold_low'] = thold_build_cdef($cdef, $thold_data['thold_low'], $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
$thold_data['thold_warning_hi'] = thold_build_cdef($cdef, $thold_data['thold_warning_hi'], $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
$thold_data['thold_warning_low'] = thold_build_cdef($cdef, $thold_data['thold_warning_low'], $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
} elseif ($thold_data['thold_type'] == 2) {
$thold_data['time_hi'] = thold_build_cdef($cdef, $thold_data['time_hi'], $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
$thold_data['time_low'] = thold_build_cdef($cdef, $thold_data['time_low'], $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
$thold_data['time_warning_hi'] = thold_build_cdef($cdef, $thold_data['time_warning_hi'], $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
$thold_data['time_warning_low'] = thold_build_cdef($cdef, $thold_data['time_warning_low'], $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
}
}
}
/**
* thold_get_column_by_cdef - resolves a threshold's stored column value (lastread by default)
* through its graph item's CDEF, if one applies, for display purposes.
*
* @param array &$thold_data The threshold row providing the column value and CDEF lookup context.
* @param string $column Unused directly; retained for call-site documentation of which column is
* being displayed (the function always transforms 'lastread').
*
* @return mixed The CDEF-transformed value, the raw lastread value when no CDEF applies, or '-'
* when neither is available.
*/
function thold_get_column_by_cdef(&$thold_data, $column = 'lastread') {
// Check is the graph item has a cdef
if (isset($thold_data['local_data_id'])) {
$cdef = db_fetch_cell_prepared('SELECT MAX(cdef_id)
FROM graph_templates_item AS gti
INNER JOIN data_template_rrd AS dtr
ON gti.task_item_id = dtr.id
WHERE local_graph_id = ?
AND dtr.id = ?
AND dtr.data_source_name = ?',
[
$thold_data['local_graph_id'],
$thold_data['data_template_rrd_id'],
$thold_data['data_source_name']
]
);
if ($cdef > 0) {
return thold_build_cdef($cdef, $thold_data['lastread'], $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
}
if (isset($thold_data['lastread'])) {
return $thold_data['lastread'];
}
}
return '-';
}
/**
* thold_format_number - formats a numeric value with locale-aware thousands separators and an
* optional SI/binary magnitude suffix (m/u/n/p for sub-unit values, K/M/G/T/etc. for large ones)
* or a fixed units suffix.
*
* @param mixed $value The numeric value to format.
* @param int $digits The number of decimal digits to display.
* @param int $baseu The magnitude base to scale by (1024 for binary, otherwise decimal/1000).
* @param bool $show_suffix Whether to append the binary 'i' suffix (e.g. 'Ki') when $baseu is 1024.
* @param bool $show_units Whether to auto-scale and append a magnitude suffix at all.
* @param string $units_suffix A fixed unit suffix to append instead of auto-scaling.
*
* @return string The formatted value, or '-' when $value isn't numeric.
*/
function thold_format_number($value, $digits = 2, $baseu = 1024, $show_suffix = true, $show_units = false, $units_suffix = '') {
$units = '';
$suffix = '';
if ($baseu == 1024 && $show_suffix) {
$suffix = 'i';
}
if (!is_numeric($value)) {
return '-';
}
if (empty($baseu)) {
$baseu = 1000;
}
if ($value == '0') {
return '0';
}
if (!$show_units && $units_suffix == '') {
return number_format_i18n($value, $digits, $baseu);
}
if ($units_suffix != '') {
$suffix = " $units_suffix";
}
if (abs($value) < 1) {
$units = ' m';
$value *= $baseu;
if (abs($value) < 1) {
$units = ' &#181;';
$value *= $baseu;
} else {
return number_format_i18n($value, $digits, $baseu) . $units . $suffix;
}
if (abs($value) < 1) {
$units = ' n';
$value *= $baseu;
} else {
return number_format_i18n($value, $digits, $baseu) . $units . $suffix;
}
if (abs($value) < 1) {
$units = ' p';
$value *= $baseu;
} else {
return number_format_i18n($value, $digits, $baseu) . $units . $suffix;
}
} else {
if (abs($value) >= $baseu) {
$units = ' K';
$value /= $baseu;
} else {
return number_format_i18n($value, $digits, $baseu) . $units . $suffix;
}
if (abs($value) >= $baseu) {
$units = ' M';
$value /= $baseu;
} else {
return number_format_i18n($value, $digits, $baseu) . $units . $suffix;
}
if (abs($value) >= $baseu) {
$units = ' G';
$value /= $baseu;
} else {
return number_format_i18n($value, $digits, $baseu) . $units . $suffix;
}
if (abs($value) >= $baseu) {
$units = ' T';
$value /= $baseu;
} else {
return number_format_i18n($value, $digits, $baseu) . $units . $suffix;
}
if (abs($value) >= $baseu) {
$units = ' P';
$value /= $baseu;
} else {
return number_format_i18n($value, $digits, $baseu) . $units . $suffix;
}
}
return number_format_i18n($value, $digits, $baseu) . $units . $suffix;
}
/**
* get_reference_types - builds a map of RRA step intervals (in seconds) eligible for a
* 'reference' time-based lookup to their display label, based on the RRA archives configured
* (optionally scoped to a single data source).
*
* @param int $local_data_id Optional local data source id to scope the eligible RRAs to.
*
* @global array $config Cacti's global configuration array.
* @global array $timearray Poller-interval-multiple code => display label map (loaded from
* arrays.php).
*
* @return array Step-interval-in-seconds => display label map.
*/
function get_reference_types($local_data_id = 0) {
global $config, $timearray;
include($config['base_path'] . '/plugins/thold/includes/arrays.php');
$poller_interval = read_config_option('poller_interval');
$rra_steps = db_fetch_assoc('SELECT DISTINCT dsp.step * dspr.steps AS frequency
FROM data_source_profiles AS dsp
INNER JOIN data_source_profiles_rra AS dspr
ON dsp.id = dspr.data_source_profile_id
LEFT JOIN data_template_data AS dtd
ON dtd.data_source_profile_id=dspr.data_source_profile_id
WHERE dspr.steps > 1 ' . ($local_data_id > 0 ? "AND dtd.local_data_id = $local_data_id" : '') . '
ORDER BY frequency');
$reference_types = [];
if (cacti_sizeof($rra_steps)) {
foreach ($rra_steps as $rra_step) {
$seconds = $rra_step['frequency'];
$setting = round($rra_step['frequency'] / $poller_interval, 0);
if (isset($timearray[$setting])) {
$reference_types[$seconds] = $timearray[$setting];
}
}
}
return $reference_types;
}
/**
* logger - sends a message to syslog at the configured (or overridden) priority/facility, used for
* threshold breach/restoral syslog notifications.
*
* @param string $subject The message subject/summary text.
* @param string $urlbreach Additional detail text appended to the syslog message.
* @param int|string $syslog_priority The syslog priority to use; falls back to the
* 'thold_syslog_priority' setting (or LOG_WARNING if that's invalid).
* @param int|string $syslog_facility The syslog facility to use; falls back to the
* 'thold_syslog_facility' setting (or LOG_DAEMON if unset).
*
* @return void
*/
function logger($subject, $urlbreach, $syslog_priority = '', $syslog_facility = '') {
if ($syslog_priority == '') {
$syslog_priority = read_config_option('thold_syslog_priority');
}
if ($syslog_facility == '') {
$syslog_facility = read_config_option('thold_syslog_facility');
}
if ($syslog_priority > 7 || $syslog_priority < 0 || $syslog_priority == '') {
$syslog_priority = LOG_WARNING;
set_config_option('thold_syslog_priority', $syslog_priority);
}
if ($syslog_facility == '') {
$syslog_facility = LOG_DAEMON;
set_config_option('thold_syslog_facility', $syslog_facility);
}
openlog('CactiTholdLog', LOG_PID | LOG_PERROR, $syslog_facility);
syslog($syslog_priority, $subject . ' - ' . $urlbreach);
if (function_exists('closelog')) {
closelog();
}
}
/**
* ack_logging - records a threshold acknowledgment: optionally sends a syslog notice, writes a
* plugin_thold_log entry, and writes a Cacti log line, all noting any operator comments.
*
* @param int $thold_id The threshold id being acknowledged.
* @param string $desc Optional operator comment to include in the log entries.
*
* @return void
*/
function ack_logging($thold_id, $desc = '') {
$thold_data = db_fetch_row_prepared('SELECT name_cache, host_id, thold_hi, thold_low,
syslog_enabled, syslog_facility, syslog_priority, lastread, local_graph_id
FROM thold_data
WHERE id = ?',
[$thold_id]);
if ($thold_data['syslog_enabled']) {
openlog('CactiTholdLog', LOG_PID | LOG_PERROR, $thold_data['syslog_facility']);
syslog($thold_data['syslog_priority'], 'Threshold ' . $thold_id . ' has been acknowledged. Additional Comments: ' . $desc);
}
$status = 99;
thold_log(
[
'type' => 99,
'time' => time(),
'host_id' => $thold_data['host_id'],
'local_graph_id' => $thold_data['local_graph_id'],
'threshold_id' => $thold_id,
'threshold_value' => '',
'current' => $thold_data['lastread'],
'status' => $status,
'description' => 'Threshold Name ' . $thold_data['name_cache'] . ' has been acknowledged. Additional Comments: "' . $desc . '"',
'emails' => '',
'bcc_emails' => ''
]
);
cacti_log('Threshold TH[' . $thold_id . '] has been acknowledged. Additional Comments: ' . $desc);
}
/**
* thold_cdef_get_usable - finds the cdef ids that include a `CURRENT_DATA_SOURCE` item, i.e. the
* CDEFs usable for threshold value transformation.
*
* @return array The list of usable cdef ids.
*/
function thold_cdef_get_usable() {
$cdef_items = db_fetch_assoc('SELECT *
FROM cdef_items
WHERE value = "CURRENT_DATA_SOURCE"
ORDER BY cdef_id');
$cdef_usable = [];
if (cacti_sizeof($cdef_items)) {
foreach ($cdef_items as $cdef_item) {
$cdef_usable[] = $cdef_item['cdef_id'];
}
}
return $cdef_usable;
}
/**
* thold_cdef_select_usable_names - builds a cdef id => name drop-down options array restricted to
* the CDEFs thold_cdef_get_usable() considers usable, plus a blank 'none' entry.
*
* @return array cdef id => name map, with id 0 mapped to an empty (no CDEF) option.
*/
function thold_cdef_select_usable_names() {
$ids = thold_cdef_get_usable();
$cdefs = db_fetch_assoc('SELECT id, name FROM cdef');
$cdef_names[0] = '';
if (cacti_sizeof($cdefs)) {
foreach ($cdefs as $cdef) {
if (in_array($cdef['id'], $ids, true)) {
$cdef_names[$cdef['id']] = $cdef['name'];
}
}
}
return $cdef_names;
}
/**
* thold_build_cdef - evaluates a graph item's CDEF RPN program against a threshold value,
* resolving Cacti's special CDEF properties (CURRENT_DATA_SOURCE, graph/data-source min/max, HDD
* total, all-data-sources sum) and `|query_*|` cache lookups as needed.
*
* @param int $cdef The cdef id to evaluate.
* @param mixed $value The raw value to transform (used as CURRENT_DATA_SOURCE).
* @param int $local_data_id The local data source id providing special-property/query context.
* @param int $data_template_rrd_id Unused directly; retained for call-site symmetry with sibling
* CDEF-aware helpers.
*
* @return mixed The CDEF-transformed value; the original value on an unrecognized CDEF property
* or RPN type; or an empty string when a required `|query_*|` operand could not be resolved.
*/
function thold_build_cdef($cdef, $value, $local_data_id, $data_template_rrd_id) {
if (!is_numeric($value)) {
return '';
}
$oldvalue = $value;
$cdefs = db_fetch_assoc_prepared('SELECT *
FROM cdef_items
WHERE cdef_id = ?
ORDER BY sequence',
[$cdef]);
$cdef_array = [];
// Set when a query operand cannot be resolved; 0 stands in so the RPN
// stack machine below can still run, but the final result fails closed.
$cdef_unavailable = false;
if (cacti_sizeof($cdefs)) {
foreach ($cdefs as $cdef) {
$prev_value = $cdef['value'];
if ($cdef['type'] == 4) {
$found = false;
$cdef['type'] = 6;
if ($cdef['value'] != '') {
if (strpos($cdef['value'], 'CURRENT_DATA_SOURCE') !== false) {
$cdef['value'] = str_replace('CURRENT_DATA_SOURCE', $oldvalue, $cdef['value']);
$found = true;
}
if (strpos($cdef['value'], 'CURRENT_GRAPH_MAXIMUM_VALUE') !== false) {
$cdef['value'] = str_replace('CURRENT_GRAPH_MAXIMUM_VALUE', get_current_value($local_data_id, 'upper_limit'), $cdef['value']);
$found = true;
}
if (strpos($cdef['value'], 'CURRENT_GRAPH_MINIMUM_VALUE') !== false) {
$cdef['value'] = str_replace('CURRENT_GRAPH_MINIMUM_VALUE', get_current_value($local_data_id, 'lower_limit'), $cdef['value']);
$found = true;
}
if (strpos($cdef['value'], 'CURRENT_DS_MINIMUM_VALUE') !== false) {
$cdef['value'] = str_replace('CURRENT_DS_MINIMUM_VALUE', get_current_value($local_data_id, 'rrd_minimum'), $cdef['value']);
$found = true;
}
if (strpos($cdef['value'], 'CURRENT_DS_MAXIMUM_VALUE') !== false) {
$cdef['value'] = str_replace('CURRENT_DS_MAXIMUM_VALUE', get_current_value($local_data_id, 'rrd_maximum'), $cdef['value']);
$found = true;
}
if (strpos($cdef['value'], 'VALUE_OF_HDD_TOTAL') !== false) {
$cdef['value'] = str_replace('VALUE_OF_HDD_TOTAL', get_current_value($local_data_id, 'hdd_total'), $cdef['value']);
$found = true;
}
if (strpos($cdef['value'], 'ALL_DATA_SOURCES_NODUPS') !== false || strpos($cdef['value'], 'ALL_DATA_SOURCES_DUPS') !== false) {
$found = true;
$total = 0;
$all_dsns = db_fetch_assoc_prepared('SELECT data_source_name
FROM data_template_rrd
WHERE local_data_id = ?',
[$local_data_id]);
if (cacti_sizeof($all_dsns)) {
foreach ($all_dsns as $dsn) {
$total += get_current_value($local_data_id, $dsn['data_source_name']);
}
}
$cdef['value'] = str_replace('ALL_DATA_SOURCES_NODUPS', $total, $cdef['value']);
$cdef['value'] = str_replace('ALL_DATA_SOURCES_DUPS', $total, $cdef['value']);
}
}
if (!$found) {
cacti_log('WARNING: Issues with CDEF property: ' . $cdef['value'] . ', CDEF ID: ' . $cdef['id'] . ', Returning Original Data: ' . $oldvalue, false, 'THOLD', POLLER_VERBOSITY_MEDIUM);
return $oldvalue;
break;
}
} elseif ($cdef['type'] == 6) {
if ($cdef['value'] != '') {
$regresult = preg_match('/^\|query_([A-Za-z0-9_]+)\|$/', $cdef['value'], $matches);
if ($regresult > 0) {
$cdef['value'] = db_fetch_cell_prepared('SELECT hsc.field_value
FROM data_local AS dl
INNER JOIN host_snmp_cache AS hsc
ON hsc.host_id = dl.host_id
AND hsc.snmp_query_id = dl.snmp_query_id
AND hsc.snmp_index = dl.snmp_index
WHERE dl.id = ?
AND hsc.field_name = ?',
[$local_data_id, $matches[1]]);
if ($cdef['value'] == '' || !is_numeric($cdef['value'])) {
$cdef['value'] = 0;
$cdef_unavailable = true;
}
}
}
}
$cdef_array[] = $cdef;
if ($cdef['type'] == 6 && !is_numeric($cdef['value'])) {
cacti_log("WARNING: Value for Data ID $local_data_id is not numeric:'" . $cdef['value'] . "', Previous Value was:'" . $prev_value . "'", false, 'THOLD', POLLER_VERBOSITY_MEDIUM);
}
}
}
$x = cacti_count($cdef_array);
if ($x == 0) {
return $oldvalue;
}
$stack = []; // operation stack for RPN
array_push($stack, $cdef_array[0]); // first one always goes on
$cursor = 1; // current pointer through RPN operations list
while ($cursor < $x) {
$type = $cdef_array[$cursor]['type'];
switch($type) {
case 6:
array_push($stack, $cdef_array[$cursor]);
break;
case 2:
// this is a binary operation. pop two values, and then use them.
$v1 = thold_expression_rpn_pop($stack);
$v2 = thold_expression_rpn_pop($stack);
$result = thold_rpn($v2['value'], $v1['value'], $cdef_array[$cursor]['value'], $local_data_id);
// put the result back on the stack.
array_push($stack, ['type' => 6, 'value' => $result]);
break;
default:
cacti_log('Unknown RPN type: ' . $cdef_array[$cursor]['type'], false, 'THOLD', POLLER_VERBOSITY_MEDIUM);
return ($oldvalue);
break;
}
$cursor++;
}
if ($cdef_unavailable) {
return '';
}
return $stack[0]['value'];
}
/**
* thold_rpn - applies one binary RPN arithmetic operator (add/subtract/multiply/divide/modulo,
* selected by numeric code) used by thold_build_cdef()'s CDEF evaluation stack machine.
*
* @param mixed $x The left operand ('U'/empty is treated as 0).
* @param mixed $y The right operand ('U'/empty is treated as 0).
* @param int $z The operator code: 1=add, 2=subtract, 3=multiply, 4=divide, 5=modulo.
* @param int $local_data_id Used only for the local data source id in error log lines.
*
* @return mixed The operation result, or an empty string on a non-numeric operand or a
* division/modulo by zero.
*/
function thold_rpn($x, $y, $z, $local_data_id = 0) {
if (empty($x) || $x == 'U') {
$x = 0;
}
if (empty($y) || $y == 'U') {
$y = 0;
}
if (!is_numeric($x)) {
cacti_log("WARNING: Erroneous CDEF logic, the first value should be numeric, but is '$x'. Data ID $local_data_id", false, 'THOLD');
return '';
}
if (!is_numeric($y)) {
cacti_log("WARNING: Erroneous CDEF logic, the second value should be numeric, but is '$y'. Data ID $local_data_id", false, 'THOLD');
return '';
}
switch ($z) {
case 1:
return $x + $y;
break;
case 2:
return $x - $y;
break;
case 3:
return $x * $y;
break;
case 4:
if ($y == 0) {
cacti_log("WARNING: Erroneous CDEF logic, division by zero. Data ID $local_data_id", false, 'THOLD');
return '';
}
return $x / $y;
break;
case 5:
if ((int) $y == 0) {
cacti_log("WARNING: Erroneous CDEF logic, modulo by zero. Data ID $local_data_id", false, 'THOLD');
return '';
}
return $x % $y;
break;
}
return '';
}
/**
* delete_old_thresholds - removes thresholds whose backing data source no longer exists (an
* orphaned data_template_rrd_id), logging each auto-delete before removing it.
*
* @return void
*/
function delete_old_thresholds() {
$tholds = db_fetch_assoc('SELECT td.id, td.data_template_rrd_id, td.local_data_id
FROM thold_data AS td
LEFT JOIN data_template_rrd AS dtr
ON dtr.id=td.data_template_rrd_id
WHERE dtr.local_data_id IS NULL');
if (cacti_sizeof($tholds)) {
foreach ($tholds as $thold_data) {
plugin_thold_log_changes($thold_data['id'], 'deleted', ['message' => 'Auto-delete due to Data Source removal']);
thold_api_thold_remove($thold_data['id']);
}
}
}
/**
* thold_api_thold_remove - deletes a threshold and its associated contact/daemon-data rows.
*
* @param int $id The threshold id to remove.
*
* @return void
*/
function thold_api_thold_remove($id) {
db_execute_prepared('DELETE FROM thold_data
WHERE id = ?',
[$id]);
db_execute_prepared('DELETE FROM plugin_thold_threshold_contact
WHERE thold_id = ?',
[$id]);
db_execute_prepared('DELETE FROM plugin_thold_daemon_data
WHERE id = ?',
[$id]);
}
/**
* thold_api_thold_template_remove - deletes a threshold template and its contact rows, and
* detaches any thresholds still referencing it.
*
* @param int $id The threshold template id to remove.
*
* @return void
*/
function thold_api_thold_template_remove($id) {
db_execute_prepared('DELETE FROM thold_template
WHERE id = ?',
[$id]);
db_execute_prepared('DELETE FROM plugin_thold_template_contact
WHERE template_id = ?',
[$id]);
db_execute_prepared('UPDATE thold_data
SET thold_template_id = 0, template_enabled = "off"
WHERE thold_template_id = ?',
[$id]);
}
/**
* thold_rrd_last - fetches the last-updated timestamp recorded in a data source's RRD file.
*
* @param int $local_data_id The local data source id whose RRD file to check.
*
* @return string The RRD's last-update Unix timestamp, as returned by rrdtool.
*/
function thold_rrd_last($local_data_id) {
$last_time_entry = @rrdtool_execute('last ' . trim(get_data_source_path($local_data_id, true)), false, RRDTOOL_OUTPUT_STDOUT);
return trim($last_time_entry);
}
/**
* get_current_value - fetches a data source's most recent RRD sample directly from its RRD file
* (flushing Boost's pending output first, if enabled), optionally CDEF-transformed.
*
* @param int $local_data_id The local data source id to fetch a sample for.
* @param string $data_template_rrd_id The RRD field name to read from the fetch result.
* @param int $cdef Optional cdef id to transform the raw value through.
* @param mixed $missing_value The value to return when no usable sample is found.
*
* @return mixed The most recent sample value (rounded to 4 decimals), or $missing_value when the
* data source, field, or a usable sample doesn't exist.
*/
function get_current_value($local_data_id, $data_template_rrd_id, $cdef = 0, $missing_value = 0) {
// get the information to populate into the rrd files
if (function_exists('boost_check_correct_enabled') && boost_check_correct_enabled()) {
boost_process_poller_output($local_data_id);
}
$last_time_entry = thold_rrd_last($local_data_id);
// This should fix and 'did you really mean month 899 errors', this is because your RRD has not polled yet.
// A missing or unreadable RRD makes rrdtool print nothing, which is not a timestamp either.
if (!is_numeric($last_time_entry) || $last_time_entry == -1) {
$last_time_entry = time();
}
$data_template_data = db_fetch_row_prepared('SELECT *
FROM data_template_data
WHERE local_data_id = ?',
[$local_data_id]);
$step = $data_template_data['rrd_step'];
// Round down to the nearest 100
$last_time_entry = (intval($last_time_entry / 100) * 100) - $step;
$last_needed = $last_time_entry + $step;
$result = rrdtool_function_fetch($local_data_id, trim($last_time_entry), trim($last_needed));
// Return Blank if the data source is not found (Newly created?)
if (!isset($result['data_source_names'])) {
return $missing_value;
}
// array_search() reports a miss as false. Testing for null let the miss
// through, and $result['values'][false] then read index 0, so a lookup for
// a data source that does not exist returned the first one's value.
$idx = array_search($data_template_rrd_id, $result['data_source_names'], true);
// Return Blank if the value was not found (Cache Cleared?)
if ($idx === false || !isset($result['values'][$idx]) || !cacti_sizeof($result['values'][$idx])) {
return $missing_value;
}
$value = array_values($result['values'][$idx])[0];
if ($cdef > 0) {
$value = thold_build_cdef($cdef, $value, $local_data_id, $data_template_rrd_id);
}
// A stored 'U'/'nan'/blank reading, or a fail-closed CDEF result, must stay missing rather than crash or coerce to zero.
if (!is_numeric($value)) {
return $missing_value;
}
return round($value, 4);
}
/**
* thold_get_ref_values - looks up a data source's historical reference value (min/max/average over
* a past window) for baseline threshold comparisons.
*
* @param int $local_data_id The local data source id to look up.
* @param string $data_source_name The RRD field name to look up within the statistics result.
* @param int $current_time The current Unix timestamp the reference window is measured from.
* @param int $prev_time How many seconds back the reference window starts.
* @param string $avg_of_cf The consolidation function to average over.
*
* @return mixed The reference statistics for the data source, or false when unavailable.
*/
function thold_get_ref_values($local_data_id, $data_source_name, $current_time, $prev_time, $avg_of_cf) {
$data = thold_get_rrd_statistics($local_data_id, $current_time, $prev_time, $avg_of_cf);
if (!isset($data[$data_source_name])) {
return false;
}
return $data[$data_source_name];
}
/**
* thold_check_exception_periods
*
* @to-do: This function should check 'globally' declared exceptions, like
* holidays etc., as well as exceptions bound to the specific $local_data_id. $local_data_id
* should inherit exceptions that are assigned on the higher level (i.e. device).
*
* @param mixed $local_data_id
* @param mixed $ref_time
* @param mixed $ref_range
*/
function thold_check_exception_periods($local_data_id, $ref_time, $ref_range) {
// TO-DO
// Check if the reference time falls into global exceptions
// Check if the current time falls into global exceptions
// Check if $local_data_id + $data_template_rrd_id have an exception (again both reference time and current time)
// Check if there are inheritances
// More on the exception concept:
// -Exceptions can be one time and recurring
// -Exceptions can be global and assigned to:
// -templates
// -devices
// -data sources
//
return false;
}
/**
* thold_check_baseline -
* Should be called after hard limits have been checked and only when they are OK
*
* The function "goes back in time" $ref_time seconds and retrieves the data
* for $ref_range seconds. Then it finds minimum and maximum values and calculates
* allowed deviations from those values.
*
* @param $local_data_id - the data source to check the data
* @param $data_source_name - the data source name to gather data from
* @param $current_value - the current data source value
* @param $thold_data - The thold data structure
*
* @return integer - integer value that indicates status
* -2 if the exception is active
* -1 if the reference value is not available
* 0 if the current value is within the boundaries
* 1 if the current value is below the calculated threshold
* 2 if the current value is above the calculated threshold
*/
function thold_check_baseline($local_data_id, $data_source_name, $current_value, &$thold_data) {
global $debug;
$now = time();
$poller_interval = read_config_option('poller_interval');
if (empty($poller_interval)) {
$poller_interval = 300;
}
$current_time = time() - $poller_interval;
$prev_time = $current_time - $thold_data['bl_ref_time_range'];
$avg_of_cf = '';
if ($thold_data['bl_type'] < 4) {
if ($thold_data['bl_ref_time_range'] > 86400 * 2) {
$current_time = $prev_time + 86400;
} elseif ($thold_data['bl_ref_time_range'] > 3600 * 2) {
$current_time = $prev_time + 3600;
} else {
$current_time = $prev_time;
}
} elseif ($thold_data['bl_type'] == 5 || $thold_data['bl_type'] == 7) {
$avg_of_cf = $thold_data['bl_cf'];
}
$ref_values = thold_get_ref_values($thold_data['local_data_id'], $data_source_name, $current_time, $prev_time, $avg_of_cf);
if ($ref_values === false || cacti_sizeof($ref_values) == 0) {
cacti_log(sprintf('WARNING: RRDtool was unable to return any reference values to TH[%s]', $thold_data['id']), false, 'THOLD');
return -1;
}
$ref_value_min = 0;
$ref_value_max = 0;
$ref_value_avg = 0;
$ref_value_last = 0;
$ref_value_cfa = 0;
if (isset($ref_values['MIN'])) {
$ref_value_min = $ref_values['MIN'];
}
if (isset($ref_values['MAX'])) {
$ref_value_max = $ref_values['MAX'];
}
if (isset($ref_values['AVG'])) {
$ref_value_avg = $ref_values['AVG'];
}
if (isset($ref_values['LAST'])) {
$ref_value_last = $ref_values['LAST'];
}
if (isset($ref_values['CFA'])) {
$ref_value_cfa = $ref_values['CFA'];
}
if ($thold_data['cdef'] > 0 && $thold_data['data_type'] == 1) {
if ($ref_value_min == '-nan' || $ref_value_max == '-nan' || $ref_value_avg == '-nan') {
cacti_log("WARNING: CDEF values are -nan: MIN $ref_value_min MAX $ref_value_max AVG $ref_value_avg LAST $ref_value_last " .
" Threshold {$thold_data['id']}:{$thold_data['name_cache']} :" . __FILE__ . ' ' . __FUNCTION__ . ':' . __LINE__, false, 'THOLD');
}
$ref_value_min = thold_build_cdef($thold_data['cdef'], $ref_value_min, $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
$ref_value_max = thold_build_cdef($thold_data['cdef'], $ref_value_max, $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
$ref_value_avg = thold_build_cdef($thold_data['cdef'], $ref_value_avg, $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
$ref_value_last = thold_build_cdef($thold_data['cdef'], $ref_value_last, $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
$ref_value_cfa = thold_build_cdef($thold_data['cdef'], $ref_value_cfa, $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
}
db_execute_prepared('UPDATE thold_data
SET bl_reference_min = ?, bl_reference_max = ?, bl_reference_avg = ?, bl_reference_last = ?
WHERE id = ?',
[$ref_value_min, $ref_value_max, $ref_value_avg, $ref_value_last, $thold_data['id']]);
$blt_low = '';
$blt_high = '';
// setup for CF based deviations
switch($thold_data['bl_cf']) {
case 'AVG':
$ref_value = $ref_value_avg;
break;
case 'MIN':
$ref_value = $ref_value_min;
break;
case 'MAX':
$ref_value = $ref_value_max;
break;
case 'LAST':
$ref_value = $ref_value_last;
break;
}
/**
* Baseline types
*
* 0 - % Deviation of Min/Max from Time in the past 'LAST'
* 1 - % Deviation of CF in the past
* 2 - Absolute Value Deviation of Min/Max from Time in the past
* 3 - Absolute Value Deviation of CF from Time in the past
* 4 - % Deviation from Floating Average of over the time period
* 5 - % Deviation from Floating Average of CF over time period
* 6 - Absolute Value Deviation of Floating Average over the time period
* 7 - Absolute Value Deviation of CF over the time period
*/
if ($thold_data['bl_type'] == 0) {
if ($thold_data['bl_pct_down'] != '' && is_numeric($ref_value_min)) {
$blt_low = $ref_value_min - (abs($ref_value_min) * $thold_data['bl_pct_down'] / 100);
}
if ($thold_data['bl_pct_up'] != '' && is_numeric($ref_value_max)) {
$blt_high = $ref_value_max + (abs($ref_value_max) * $thold_data['bl_pct_up'] / 100);
}
} elseif ($thold_data['bl_type'] == 1) {
if ($thold_data['bl_pct_down'] != '' && is_numeric($ref_value)) {
$blt_low = $ref_value - (abs($ref_value) * $thold_data['bl_pct_down'] / 100);
}
if ($thold_data['bl_pct_up'] != '' && is_numeric($ref_value)) {
$blt_high = $ref_value + (abs($ref_value) * $thold_data['bl_pct_up'] / 100);
}
} elseif ($thold_data['bl_type'] == 2) {
if ($thold_data['bl_pct_down'] != '' && is_numeric($ref_value_min)) {
$blt_low = $ref_value_min - $thold_data['bl_pct_down'];
}
if ($thold_data['bl_pct_up'] != '' && is_numeric($ref_value_max)) {
$blt_high = $ref_value_max + $thold_data['bl_pct_up'];
}
} elseif ($thold_data['bl_type'] == 3) {
if ($thold_data['bl_pct_down'] != '' && is_numeric($ref_value)) {
$blt_low = $ref_value - $thold_data['bl_pct_down'];
}
if ($thold_data['bl_pct_up'] != '' && is_numeric($ref_value)) {
$blt_high = $ref_value + $thold_data['bl_pct_up'];
}
} elseif ($thold_data['bl_type'] == 4) {
if ($thold_data['bl_pct_down'] != '' && is_numeric($ref_value_avg)) {
$blt_low = $ref_value_avg - (abs($ref_value_avg) * $thold_data['bl_pct_down'] / 100);
}
if ($thold_data['bl_pct_up'] != '' && is_numeric($ref_value_avg)) {
$blt_high = $ref_value_avg + (abs($ref_value_avg) * $thold_data['bl_pct_up'] / 100);
}
} elseif ($thold_data['bl_type'] == 5) {
if ($thold_data['bl_pct_down'] != '' && is_numeric($ref_value_avg)) {
$blt_low = $ref_value_avg - (abs($ref_value_avg) * $thold_data['bl_pct_down'] / 100);
}
if ($thold_data['bl_pct_up'] != '' && is_numeric($ref_value_avg)) {
$blt_high = $ref_value_avg + (abs($ref_value_avg) * $thold_data['bl_pct_up'] / 100);
}
} elseif ($thold_data['bl_type'] == 6) {
if ($thold_data['bl_pct_down'] != '' && is_numeric($ref_value_avg)) {
$blt_low = $ref_value_avg - $thold_data['bl_pct_down'];
}
if ($thold_data['bl_pct_up'] != '' && is_numeric($ref_value_avg)) {
$blt_high = $ref_value_avg + $thold_data['bl_pct_up'];
}
} else {
if ($thold_data['bl_pct_down'] != '' && is_numeric($ref_value_cfa)) {
$blt_low = $ref_value_cfa - $thold_data['bl_pct_down'];
}
if ($thold_data['bl_pct_up'] != '' && is_numeric($ref_value_cfa)) {
$blt_high = $ref_value_cfa + $thold_data['bl_pct_up'];
}
}
// Cache the calculated or empty values
$thold_data['thold_low'] = $blt_low;
$thold_data['thold_hi'] = $blt_high;
$thold_data['bl_thold_valid'] = 0;
$failed = 0;
// Check low boundary
if ($blt_low != '' && $current_value < $blt_low) {
$failed = 1;
}
// Check up boundary
if ($failed == 0 && $blt_high != '' && $current_value > $blt_high) {
$failed = 2;
}
if ($debug) {
print 'Local Data Id: ' . $local_data_id . ':' . $thold_data['data_template_rrd_id'] . "\n";
print 'Ref. values count: ' . (isset($ref_values) ? count($ref_values) : 'N/A') . "\n";
print 'Ref. value (min): ' . (isset($ref_value_min) ? $ref_value_min : 'N/A') . "\n";
print 'Ref. value (max): ' . (isset($ref_value_max) ? $ref_value_max : 'N/A') . "\n";
print 'Cur. value: ' . $current_value . "\n";
print 'Low bl thresh: ' . (isset($blt_low) ? $blt_low : 'N/A') . "\n";
print 'High bl thresh: ' . (isset($blt_high) ? $blt_high : 'N/A') . "\n";
print 'Check against baseline: ';
switch($failed) {
case 0:
print 'OK';
break;
case 1:
print 'FAIL: Below baseline threshold!';
break;
case 2:
print 'FAIL: Above baseline threshold!';
break;
}
print "\n";
print "------------------\n";
}
return $failed;
}
/**
* get_bl_type - renders a baseline threshold type's abbreviated display label, substituting the
* configured consolidation function into types that reference one.
*
* @param string $type The baseline type code (0-7).
* @param string $cf The consolidation function name to substitute into TIP/AOT-style labels.
*
* @global array $bl_types Baseline type code => full display label map.
*
* @return string The abbreviated display label for the type.
*/
function get_bl_type($type, $cf) {
global $bl_types;
switch($type) {
case '0':
case '2':
case '4':
case '6':
return str_replace(['Deviation', 'Absolute'], ['Dev', 'Abs'], $bl_types[$type]);
break;
case '1':
return __('%% Dev [TIP:%s]', $cf, 'thold');
break;
case '3':
return __('Abs Val [TIP:%s]', $cf, 'thold');
break;
case '5':
return __('%%% Dev [AOT:%s]', $cf, 'thold');
break;
case '7':
return __('Abs Val [AOT:%s]', $cf, 'thold');
break;
}
}
/**
* thold_create_new_graph_from_template - handles the "create graph from host template/SNMP index"
* form submission on the threshold graph-creation page, either delegating to Cacti core's new-
* graphs UI (to collect the graph selections) or saving the selected graphs once confirmed.
*
* @return void
*/
function thold_create_new_graph_from_template() {
if (!isset_request_var('host_id')) {
$device_id = 0;
} else {
$device_id = get_filter_request_var('host_id');
}
if (isset_request_var('save_component_graph')) {
// summarize the 'create graph from host template/snmp index' stuff into an array
foreach ($_POST as $var => $val) {
if (preg_match('/^cg_(\d+)$/', $var, $matches)) {
$selected_graphs['cg'][$matches[1]][$matches[1]] = true;
} elseif (preg_match('/^cg_g$/', $var)) {
if (get_request_var('cg_g') > 0) {
$selected_graphs['cg'][get_request_var('cg_g')][get_request_var('cg_g')] = true;
}
} elseif (preg_match('/^sg_(\d+)_([a-f0-9]{32})$/', $var, $matches)) {
$selected_graphs['sg'][$matches[1]][get_nfilter_request_var('sgg_' . $matches[1])][$matches[2]] = true;
}
}
if (!isset_request_var('host_template_id')) {
$host_template_id = 0;
} else {
$host_template_id = get_filter_request_var('host_template_id');
}
if (isset($selected_graphs)) {
html_graph_new_graphs('thold.php', $device_id, $host_template_id, $selected_graphs);
exit;
}
} elseif (isset_request_var('save_component_new_graphs')) {
thold_new_graphs_save($device_id);
}
}
/**
* SI suffixes thold accepts on a threshold bound, smallest first.
*
* thold_display_to_raw() and thold_raw_to_display() are inverses of each
* other, so they read the same table rather than each carrying their own
* copy. They previously disagreed: 'p' scaled by 1e-9 on the way in while
* 1e-12 rendered as 'f' on the way out, so a bound was divided by a thousand
* every time its form was opened and saved.
*
* @return array<string, float> Suffix to the factor it multiplies by.
*/
function thold_unit_suffixes() {
static $suffixes = [
'y' => 1e-24,
'z' => 1e-21,
'a' => 1e-18,
'f' => 1e-15,
'p' => 1e-12,
'n' => 1e-9,
'u' => 1e-6,
'm' => 1e-3,
'K' => 1e3,
'M' => 1e6,
'G' => 1e9,
'T' => 1e12,
'P' => 1e15,
'E' => 1e18,
'Z' => 1e21,
'Y' => 1e24,
];
return $suffixes;
}
/**
* thold_display_to_raw - converts a displayed number with an optional SI-style unit suffix (e.g.
* '100M') into its raw numeric value (e.g. 100000000), per thold_unit_suffixes()'s scale table.
*
* @param mixed $number The displayed value, optionally suffixed with a unit letter.
* @param mixed $field_name The form field name, used to remember the raw entry and flag errors.
*
* @return mixed The raw numeric value, or false (raising a validation message) when the suffix
* is unrecognized.
*/
function thold_display_to_raw($number, $field_name) {
$number = trim($number);
$_SESSION['sess_field_values'][$field_name] = $number;
$suffix = substr($number, -1);
// if the number is numeric just return
if (is_numeric($suffix)) {
return $number;
}
$number = trim(substr($number, 0, -1));
$suffixes = thold_unit_suffixes();
if (!is_numeric($number) || !isset($suffixes[$suffix])) {
$_SESSION['sess_error_fields'][$field_name] = $field_name;
raise_message(3);
return false;
}
return $number * $suffixes[$suffix];
}
/**
* thold_raw_to_display - converts a raw numeric value into a displayed number with the largest SI
* suffix (per thold_unit_suffixes()'s scale table) that keeps its scaled magnitude at least one;
* the inverse of thold_display_to_raw().
*
* @param mixed $number The raw numeric value to convert.
*
* @return mixed The scaled, suffixed display string, or false when $number isn't numeric.
*/
function thold_raw_to_display($number) {
if ($number != '') {
$number = trim($number);
}
if (!is_numeric($number)) {
return false;
}
if ($number == 0) {
return trim($number);
}
$multiplier = $number > 0 ? 1 : -1;
$number = abs($number);
// The largest scale that still leaves a value of one or more, where the
// empty suffix stands for a scale of one. Factors ascend, so the ratio
// falls monotonically and the last match is the one wanted.
$scales = thold_unit_suffixes();
$scales[''] = 1.0;
asort($scales, SORT_NUMERIC);
$suffix = '';
$factor = 1.0;
foreach ($scales as $candidate => $candidate_factor) {
if ($number / $candidate_factor >= 1) {
$suffix = $candidate;
$factor = $candidate_factor;
}
}
return trim((($number / $factor) * $multiplier) . $suffix);
}
/**
* save_thold - handles the threshold add/edit form submission: validates and normalizes every
* submitted field (bounds, triggers, notification lists, CDEF, contacts), then inserts or updates
* the thold_data row and its associated contact assignments.
*
* @global array $banner Used by the underlying save/validation helpers for form messaging.
*
* @return int The saved threshold's id.
*/
function save_thold() {
global $banner;
$data_query_id = get_filter_request_var('data_query_id');
$data_template_id = get_filter_request_var('data_template_id');
$graph_template_id = get_filter_request_var('graph_template_id');
$host_template_id = get_filter_request_var('host_template_id');
$thold_template_id = get_filter_request_var('thold_template_id');
if (isset_request_var('save_component_new_graphs')) {
// Correct issue where host_id variables comes back as an array
if (isset_request_var('host_id')) {
if (is_array(get_nfilter_request_var('host_id'))) {
$temp = get_nfilter_request_var('host_id');
foreach ($temp as $t) {
set_request_var('host_id', input_validate_input_number($t));
}
}
} else {
$banner = __('The Device ID was not set while trying to create Graph and Threshold', 'thold');
thold_raise_message($banner, MESSAGE_LEVEL_ERROR);
return false;
}
$device_id = get_filter_request_var('host_id');
if (isset_request_var('thold_template_id')) {
$template = db_fetch_row_prepared('SELECT *
FROM thold_template
WHERE id = ?',
[get_filter_request_var('thold_template_id')]);
if (!sizeof($template)) {
$banner = __('The Threshold Template ID was not found while trying to create Graph and Threshold', 'thold');
thold_raise_message($banner, MESSAGE_LEVEL_ERROR);
cacti_log('ERROR: The Threshold Template ID was not found', false, 'THOLD');
return false;
}
} else {
$banner = __('The Threshold Template ID was not set while trying to create Graph and Threshold', 'thold');
thold_raise_message($banner, MESSAGE_LEVEL_ERROR);
cacti_log('ERROR: The Threshold Template ID not set for save', false, 'THOLD');
return false;
}
/**
* attempt to see if the graph already exists for this thold
* if it's template based.
*/
if (isset_request_var('type_id') && get_nfilter_request_var('type_id') == 'template') {
if (get_filter_request_var('data_query_id') > 0) {
$local_graph_id = db_fetch_cell_prepared('SELECT gl.id
FROM graph_local AS gl
INNER JOIN snmp_query_graph AS sqg
ON sqg.snmp_query_id = gl.snmp_query_id
AND sqg.graph_template_id = gl.graph_template_id
WHERE sqg.id = ?
AND gl.host_id = ?
AND gl.snmp_query_id = ?
AND gl.snmp_index = ?
ORDER BY id ASC
LIMIT 1',
[
get_filter_request_var('graph_template_id'),
get_filter_request_var('host_id'),
get_filter_request_var('data_query_id'),
get_request_var('snmp_index')
]
);
if ($local_graph_id > 0) {
$graph_template_id = db_fetch_cell_prepared('SELECT graph_template_id
FROM graph_local
WHERE id = ?',
[$local_graph_id]);
set_request_var('graph_template_id', $graph_template_id);
}
} else {
$local_graph_id = db_fetch_cell_prepared('SELECT id
FROM graph_local
WHERE graph_template_id = ?
AND host_id = ?
AND snmp_query_id = ?
AND snmp_index = ?
ORDER BY id ASC
LIMIT 1',
[
get_filter_request_var('graph_template_id'),
get_filter_request_var('host_id'),
get_filter_request_var('data_query_id'),
get_request_var('snmp_index')
]
);
}
if ($local_graph_id > 0) {
$local_data = db_fetch_row_prepared('SELECT dtr.id, dtr.local_data_id
FROM data_template_rrd AS dtr
INNER JOIN graph_templates_item AS gti
ON gti.task_item_id = dtr.id
AND gti.local_graph_id = ?',
[$local_graph_id]);
if (isset($local_data['local_data_id']) && $local_data['local_data_id'] > 0) {
set_request_var('local_graph_id', $local_graph_id);
set_request_var('local_data_id', $local_data['local_data_id']);
set_request_var('data_template_rrd_id', $local_data['id']);
}
}
set_request_var('thold_per_enabled', 'on');
}
if (!isset_request_var('local_graph_id')) {
$graph_array = thold_new_graphs_save($device_id);
if ($graph_array !== false) {
if (isset($graph_array['local_graph_id'])) {
set_request_var('local_graph_id', $graph_array['local_graph_id']);
}
if (empty($data_template_id)) {
$data_template_id = db_fetch_cell_prepared('SELECT data_template_id
FROM thold_template
WHERE id = ?',
[$thold_template_id]);
}
if (isset($graph_array['local_data_id'][$data_template_id])) {
set_request_var('local_data_id', $graph_array['local_data_id'][$data_template_id]);
}
$temp = db_fetch_cell_prepared('SELECT dtr.id
FROM data_template_rrd AS dtr
WHERE local_data_id = ?
AND data_source_name = ?',
[get_request_var('local_data_id'), $template['data_source_name']]);
set_request_var('data_template_rrd_id', $temp);
} else {
$banner = __('The Graph Creation failed for Threshold Template', 'thold');
thold_raise_message($banner, MESSAGE_LEVEL_ERROR);
cacti_log('ERROR: Graph Creation failed for Threshold Template', false, 'THOLD');
return false;
}
set_request_var('thold_per_enabled', 'on');
}
if (!isset_request_var('save_autocreate') || get_filter_request_var('save_autocreate') == 1) {
$autocreated = db_fetch_cell_prepared('SELECT COUNT(*)
FROM plugin_thold_host
WHERE host_id = ?
AND thold_template_id = ?',
[$device_id, $thold_template_id]);
if ($autocreated) {
$thold = db_fetch_cell_prepared('SELECT id
FROM thold_data
WHERE local_data_id = ?
AND local_graph_id = ?
AND data_template_rrd_id = ?
AND data_source_name = ?',
[
get_request_var('local_data_id'),
get_request_var('local_graph_id'),
get_request_var('data_template_rrd_id'),
$template['data_source_name']
]
);
if ($thold > 0) {
$banner = __('Threshold was Autocreated due to Device Template mapping', 'thold');
thold_raise_message($banner, MESSAGE_LEVEL_INFO);
return $thold;
}
}
set_request_var('thold_enabled', 'on');
}
}
if (isset_request_var('my_host_id')) {
$device_id = get_filter_request_var('my_host_id');
} else {
$device_id = get_filter_request_var('host_id');
}
$local_data_id = get_filter_request_var('local_data_id');
$local_graph_id = get_filter_request_var('local_graph_id');
$data_template_rrd_id = get_filter_request_var('data_template_rrd_id');
// In cases where a Graph can have multiple RRDtool data sources
// coming from different RRDfiles, we have to reset the local
// local_data_id to match the data_template_rrd_id
if (!empty($data_template_rrd_id)) {
$local_data_id = db_fetch_cell_prepared('SELECT local_data_id
FROM data_template_rrd
WHERE id = ?',
[$data_template_rrd_id]);
}
$template_enabled = isset_request_var('template_enabled') && get_nfilter_request_var('template_enabled') == 'on' ? 'on' : 'off';
$thold_per_enabled = isset_request_var('thold_per_enabled') && get_nfilter_request_var('thold_per_enabled') == 'on' ? 'on' : '';
if ($template_enabled == 'on') {
if ($local_graph_id > 0 && !is_thold_allowed_graph($local_graph_id)) {
$banner = __('Permission Denied', 'thold');
thold_raise_message($banner, MESSAGE_LEVEL_ERROR);
return false;
}
if (get_request_var('id') > 0) {
db_execute_prepared('UPDATE thold_data
SET template_enabled = "on", thold_per_enabled = ?
WHERE id = ?',
[$thold_per_enabled, get_request_var('id')]);
}
$data = db_fetch_row_prepared('SELECT id, thold_template_id
FROM thold_data
WHERE local_data_id = ?
AND data_template_rrd_id = ?',
[$local_data_id, $data_template_rrd_id]);
thold_template_update_threshold($data['id'], $data['thold_template_id']);
$banner = __('Record Updated', 'thold');
plugin_thold_log_changes($data['id'], 'modified', ['id' => $data['id'], 'template_enabled' => 'on']);
thold_raise_message($banner, MESSAGE_LEVEL_INFO);
return get_filter_request_var('id');
}
get_filter_request_var('thold_fail_trigger');
get_filter_request_var('thold_warning_fail_trigger');
get_filter_request_var('repeat_alert');
get_filter_request_var('thold_type');
get_filter_request_var('time_fail_trigger');
get_filter_request_var('time_fail_length');
get_filter_request_var('time_warning_fail_trigger');
get_filter_request_var('time_warning_fail_length');
get_filter_request_var('data_type');
get_filter_request_var('cdef');
get_filter_request_var('notify_warning');
get_filter_request_var('notify_alert');
get_filter_request_var('bl_type');
get_filter_request_var('bl_ref_time_range');
get_filter_request_var('bl_fail_trigger');
get_filter_request_var('syslog_facility');
get_filter_request_var('syslog_priority');
$save = [];
if ($thold_template_id > 0 && !isset_request_var('id')) {
$save = $template;
unset($save['id']);
unset($save['hash']);
unset($save['suggested_name']);
unset($save['data_source_id']);
unset($save['data_template_name']);
unset($save['data_source_friendly']);
unset($save['notify_templated']);
set_request_var('thold_enabled', 'on');
set_request_var('template_enabled', 'on');
}
if (isset_request_var('id')) {
$save['id'] = get_request_var('id');
} else {
$save['id'] = '0';
}
if (isset_request_var('snmp_event_category')) {
set_request_var('snmp_event_category', trim(str_replace(['\\', "'", '"'], '', get_nfilter_request_var('snmp_event_category'))));
}
if (isset_request_var('snmp_event_description')) {
set_request_var('snmp_event_description', trim(str_replace(['\\', "'", '"'], '', get_nfilter_request_var('snmp_event_description'))));
}
if (isset_request_var('snmp_event_severity')) {
get_filter_request_var('snmp_event_severity');
}
if (isset_request_var('snmp_event_warning_severity')) {
get_filter_request_var('snmp_event_warning_severity');
}
if (!empty($data_template_rrd_id)) {
$data_source_name = db_fetch_cell_prepared('SELECT data_source_name
FROM data_template_rrd
WHERE id = ?',
[$data_template_rrd_id]);
$data_source_info = db_fetch_row_prepared('SELECT data_template_id, data_source_name
FROM data_template_rrd
WHERE id = ?',
[$data_template_rrd_id]);
} elseif (!empty($local_graph_id) && empty($graph_template_id)) {
$graph_template_id = db_fetch_cell_prepared('SELECT graph_template_id
FROM graph_local
WHERE id = ?',
[$local_graph_id]);
}
$save['host_id'] = $device_id;
$save['data_template_rrd_id'] = $data_template_rrd_id;
$save['local_data_id'] = $local_data_id;
if ($thold_template_id == 0) {
$save['thold_enabled'] = 'on';
} else {
$save['thold_enabled'] = isset_request_var('thold_enabled') && get_request_var('thold_enabled') == 'on' ? 'on' : 'off';
}
$save['thold_per_enabled'] = isset_request_var('thold_per_enabled') && get_request_var('thold_per_enabled') == 'on' ? 'on' : '';
if ($thold_template_id > 0) {
$save['thold_template_id'] = $thold_template_id;
}
$save['exempt'] = isset_request_var('exempt') ? 'on' : '';
$save['repeat_alert'] = get_filter_request_var('repeat_alert');
$save['data_template_id'] = $data_source_info['data_template_id'];
$save['data_source_name'] = $data_source_info['data_source_name'];
// Acknowledgment
if (isset_request_var('acknowledgment')) {
switch(get_nfilter_request_var('acknowledgment')) {
case 'none':
$save['reset_ack'] = '';
$save['persist_ack'] = '';
break;
case 'reset_ack':
$save['reset_ack'] = 'on';
$save['persist_ack'] = '';
break;
case 'persist_ack':
$save['reset_ack'] = '';
$save['persist_ack'] = 'on';
break;
}
} else {
$save['reset_ack'] = '';
$save['persist_ack'] = '';
}
// Syslog Settings
$save['syslog_enabled'] = isset_request_var('syslog_enabled') ? 'on' : '';
$save['syslog_priority'] = get_request_var('syslog_priority');
$save['syslog_facility'] = get_request_var('syslog_facility');
// HRULE Settings
$save['thold_hrule_warning'] = get_nfilter_request_var('thold_hrule_warning');
$save['thold_hrule_alert'] = get_nfilter_request_var('thold_hrule_alert');
$save['skipscale'] = isset_request_var('skipscale') ? 'on' : '';
$save['restored_alert'] = isset_request_var('restored_alert') ? 'on' : '';
$save['thold_type'] = get_request_var('thold_type');
$save['template_enabled'] = isset_request_var('template_enabled') && get_nfilter_request_var('template_enabled') == 'on' ? 'on' : 'off';
// High / Low
$save['thold_hi'] = trim_round_request_var('thold_hi', 4, 'thold_hi');
$save['thold_low'] = trim_round_request_var('thold_low', 4, 'thold_low');
$save['thold_fail_trigger'] = isempty_request_var('thold_fail_trigger') ? read_config_option('alert_trigger') : get_nfilter_request_var('thold_fail_trigger');
// Time Based
$save['time_hi'] = trim_round_request_var('time_hi', 4, 'time_hi');
$save['time_low'] = trim_round_request_var('time_low', 4, 'time_low');
$save['time_fail_trigger'] = isempty_request_var('time_fail_trigger') ? read_config_option('thold_warning_time_fail_trigger') : get_nfilter_request_var('time_fail_trigger');
$save['time_fail_length'] = isempty_request_var('time_fail_length') ? (read_config_option('thold_warning_time_fail_length') > 0 ?
read_config_option('thold_warning_time_fail_length') : 1) : get_nfilter_request_var('time_fail_length');
// Warning High / Low
$save['thold_warning_hi'] = trim_round_request_var('thold_warning_hi', 4, 'thold_warning_hi');
$save['thold_warning_low'] = trim_round_request_var('thold_warning_low', 4, 'thold_warning_low');
$save['thold_warning_fail_trigger'] = isempty_request_var('thold_warning_fail_trigger') ? read_config_option('alert_trigger') : get_nfilter_request_var('thold_warning_fail_trigger');
// Warning Time Based
$save['time_warning_hi'] = trim_round_request_var('time_warning_hi', 4, 'time_warning_hi');
$save['time_warning_low'] = trim_round_request_var('time_warning_low', 4, 'time_warning_low');
$save['time_warning_fail_trigger'] = isempty_request_var('time_warning_fail_trigger') ?
read_config_option('thold_warning_time_fail_trigger') : get_nfilter_request_var('time_warning_fail_trigger');
$save['time_warning_fail_length'] = isempty_request_var('time_warning_fail_length') ?
(read_config_option('thold_warning_time_fail_length') > 0 ?
read_config_option('thold_warning_time_fail_length') : 1) : get_nfilter_request_var('time_warning_fail_length');
// Baseline
$save['bl_thold_valid'] = '0';
$save['bl_type'] = get_filter_request_var('bl_type');
$save['bl_cf'] = get_nfilter_request_var('bl_cf');
$save['bl_ref_time_range'] = isempty_request_var('bl_ref_time_range') ? read_config_option('alert_bl_timerange_def') : get_nfilter_request_var('bl_ref_time_range');
$save['bl_pct_down'] = trim_round_request_var('bl_pct_down', 4, 'bl_pct_down');
$save['bl_pct_up'] = trim_round_request_var('bl_pct_up', 4, 'bl_pct_up');
$save['bl_fail_trigger'] = isempty_request_var('bl_fail_trigger') ? read_config_option('alert_bl_trigger') : get_nfilter_request_var('bl_fail_trigger');
// Notification
$save['notify_extra'] = get_nfilter_request_var('notify_extra');
$save['notify_warning_extra'] = get_nfilter_request_var('notify_warning_extra');
$save['notify_warning'] = get_filter_request_var('notify_warning');
$save['notify_alert'] = get_filter_request_var('notify_alert');
// Notes
$save['notes'] = get_nfilter_request_var('notes');
$save['external_id'] = get_nfilter_request_var('external_id');
$save['format_file'] = get_nfilter_request_var('format_file');
$save['graph_timespan'] = get_nfilter_request_var('graph_timespan');
// Data Manipulation
$save['data_type'] = get_nfilter_request_var('data_type');
$save['percent_ds'] = (isset_request_var('percent_ds')) ? get_nfilter_request_var('percent_ds') : '';
$save['cdef'] = get_filter_request_var('cdef');
$save['expression'] = (isset_request_var('expression')) ? get_nfilter_request_var('expression') : '';
$save['upper_ds'] = (isset_request_var('upper_ds')) ? get_nfilter_request_var('upper_ds') : '';
$save['show_units'] = isset_request_var('show_units') && get_request_var('show_units') == 'on' ? 'on' : 'off';
$save['units_suffix'] = get_nfilter_request_var('units_suffix');
$save['decimals'] = get_nfilter_request_var('decimals');
// Email Subjects
$save['email_subject'] = get_nfilter_request_var('email_subject');
$save['email_subject_warn'] = get_nfilter_request_var('email_subject_warn');
$save['email_subject_restoral'] = get_nfilter_request_var('email_subject_restoral');
// Email Bodies
$save['email_body'] = get_nfilter_request_var('email_body');
$save['email_body_warn'] = get_nfilter_request_var('email_body_warn');
$save['email_body_restoral'] = get_nfilter_request_var('email_body_restoral');
// Command execution
$save['trigger_cmd_high'] = get_nfilter_request_var('trigger_cmd_high');
$save['trigger_cmd_low'] = get_nfilter_request_var('trigger_cmd_low');
$save['trigger_cmd_norm'] = get_nfilter_request_var('trigger_cmd_norm');
// SNMP Information
$save['snmp_event_category'] = get_filter_request_var('snmp_event_category');
$save['snmp_event_description'] = get_nfilter_request_var('snmp_event_description');
$save['snmp_event_severity'] = isset_request_var('snmp_event_severity') ? get_nfilter_request_var('snmp_event_severity') : 4;
$save['snmp_event_warning_severity'] = isset_request_var('snmp_event_warning_severity') ? get_nfilter_request_var('snmp_event_warning_severity') : 3;
if ($local_graph_id > 0 && $graph_template_id > 0) {
$save['local_graph_id'] = $local_graph_id;
$save['graph_template_id'] = $graph_template_id;
} elseif ($local_graph_id > 0) {
$save['local_graph_id'] = $local_graph_id;
$save['graph_template_id'] = $graph_template_id;
} else {
$grapharr = db_fetch_row_prepared('SELECT DISTINCT local_graph_id, graph_template_id
FROM graph_templates_item
WHERE task_item_id = ?',
[$save['data_template_rrd_id']]);
if ($grapharr === false || count($grapharr) == 0) {
thold_raise_message(__('Failed to find linked Graph Template Item \'%d\' on Threshold \'%d\'', $save['data_template_rrd_id'], $save['id'], 'thold'), MESSAGE_LEVEL_ERROR);
return false;
}
$save['local_graph_id'] = $grapharr['local_graph_id'];
$save['graph_template_id'] = $grapharr['graph_template_id'];
}
if ($save['id'] > 0 && $save['local_graph_id'] > 0 && !is_thold_allowed_graph($save['local_graph_id'])) {
thold_raise_message(__('Permission Denied', 'thold'), MESSAGE_LEVEL_ERROR);
return false;
}
if (isset_request_var('name') && get_nfilter_request_var('name') != '') {
$name = get_nfilter_request_var('name');
} else {
$name = '|data_source_description| [|data_source_name|]';
}
$name_cache = thold_expand_string($save, $name);
$save['name'] = $name;
$save['name_cache'] = $name_cache;
$save = api_plugin_hook_function('thold_edit_save_thold', $save);
if (!thold_validate_save($save, 'thold')) {
header('Location: thold.php?action=edit&header=false&id=' . ($save['id'] > 0 ? $save['id'] : ''));
exit;
}
$id = sql_save($save , 'thold_data');
if (isempty_request_var('id')) {
set_config_option('time_last_change_thold', time());
}
if (isset_request_var('notify_accounts') && is_array(get_nfilter_request_var('notify_accounts'))) {
thold_save_threshold_contacts($id, get_nfilter_request_var('notify_accounts'));
} elseif (!isset_request_var('notify_accounts')) {
thold_save_threshold_contacts($id, []);
}
if ($id) {
if ($thold_template_id > 0 && $save['template_enabled'] == 'on') {
thold_template_update_threshold($id, $thold_template_id);
}
plugin_thold_log_changes($id, 'modified', $save);
$thold_sql = "SELECT
td.*, dtd.rrd_step, tt.name AS template_name,
dtr.data_source_name as data_source, h.hostname,
h.description, h.notes AS dnotes, h.snmp_engine_id,
IF(IFNULL(td.`lastread`,'')='',NULL,(td.`lastread` + 0.0)) as `flastread`, td.`lasttime`,
IF(IFNULL(td.`oldvalue`,'')='',NULL,(td.`oldvalue` + 0.0)) as `foldvalue`, td.`repeat_alert`,
UNIX_TIMESTAMP() - UNIX_TIMESTAMP(lastchanged) AS `instate`
FROM thold_data AS td
INNER JOIN graph_local AS gl
ON gl.id = td.local_graph_id
LEFT JOIN graph_templates AS gt
ON gt.id = gl.graph_template_id
LEFT JOIN host AS h
ON h.id = gl.host_id
LEFT JOIN thold_template AS tt
ON tt.id = td.thold_template_id
LEFT JOIN data_template_data AS dtd
ON dtd.local_data_id = td.local_data_id
LEFT JOIN data_template_rrd AS dtr
ON dtr.id = td.data_template_rrd_id
WHERE td.id = ?";
$thold = db_fetch_row_prepared($thold_sql, [$id]);
if ($save['thold_type'] == 1) {
thold_check_threshold($thold);
}
set_request_var('id', $id);
} else {
set_request_var('id', '0');
}
if ($save['id'] == 0) {
$banner = __esc('Created Threshold: %s', $save['name_cache'], 'thold');
} else {
$banner = __('Record Updated', 'thold');
}
thold_raise_message($banner, MESSAGE_LEVEL_INFO);
return $id;
}
/**
* trim_round_request_var - reads a request variable, converts it from its displayed (optionally
* unit-suffixed) form to a raw numeric value, and rounds it to the given precision.
*
* @param string $variable The request variable name to read.
* @param int $digits The number of decimal digits to round to; 0 skips rounding.
* @param string $field_name The form field name, passed through to thold_display_to_raw() for
* error tracking.
*
* @return mixed The rounded raw value, '0' for a literal zero, or an empty string when unset.
*/
function trim_round_request_var($variable, $digits = 0, $field_name = '') {
$variable = trim(get_nfilter_request_var($variable));
if (!empty($variable)) {
$variable = thold_display_to_raw($variable, $field_name);
}
if ($variable == '0') {
return '0';
}
if (empty($variable)) {
return '';
}
if ($digits > 0) {
return round($variable, $digits);
} else {
return $variable;
}
}
/**
* thold_save_template_contacts - replaces a threshold template's notification contact
* assignments with the given list.
*
* @param int $id The threshold template id to update contacts for.
* @param array $contacts The contact ids to assign to the template.
*
* @return void
*/
function thold_save_template_contacts($id, $contacts) {
db_execute_prepared('DELETE
FROM plugin_thold_template_contact
WHERE template_id = ?',
[$id]);
if (!empty($contacts)) {
foreach ($contacts as $contact) {
db_execute_prepared('INSERT INTO plugin_thold_template_contact
(template_id, contact_id)
VALUES (?, ?)',
[$id, $contact]);
}
}
}
/**
* thold_raise_message - raises a Cacti UI message and also records it (with a call stack) to the
* Cacti log at debug verbosity, under a uniquely-numbered message id per request.
*
* @param string $message The message text to display/log.
* @param int $level The message severity (one of the MESSAGE_LEVEL_* constants).
*
* @return void
*/
function thold_raise_message($message, $level = MESSAGE_LEVEL_NONE) {
static $thold_message_count = 0;
$message_id = 'thold_message_' . $thold_message_count;
cacti_log("raise_message($message_id, $message, $level);" . cacti_debug_backtrace('', false, false), false, 'THOLD', POLLER_VERBOSITY_DEBUG);
raise_message($message_id, $message, $level);
$thold_message_count++;
}
/**
* thold_save_threshold_contacts - replaces a threshold's notification contact assignments with
* the given list.
*
* @param int $id The threshold id to update contacts for.
* @param array $contacts The contact ids to assign to the threshold.
*
* @return void
*/
function thold_save_threshold_contacts($id, $contacts) {
db_execute_prepared('DELETE
FROM plugin_thold_threshold_contact
WHERE thold_id = ?',
[$id]);
foreach ($contacts as $contact) {
db_execute_prepared('INSERT INTO plugin_thold_threshold_contact
(thold_id, contact_id)
VALUES (?, ?)',
[$id, $contact]);
}
}
/**
* thold_validate_save - validates a submitted threshold/template save against its type's rules
* (hi/lo, baseline deviation, or time-based bound/trigger consistency), raising a UI error
* message and flagging the offending fields when invalid.
*
* @param array $save The submitted threshold/template data to validate.
* @param string $type Which entity is being validated: 'thold_template' or 'thold'.
*
* @return bool True when valid (or when validation is skipped for a not-yet-created threshold);
* false when one or more rules failed.
*/
function thold_validate_save($save, $type = 'thold_template') {
/**
* Type Types:
* 0 - Hi / Low
* 1 - Baseline Deviation
* 2 - Time Based
*/
$banner = '';
/**
* Only validate thresholds once they are created
* This is required due to the wizard control on the
* Thold page.
*/
if ($type == 'thold' && empty($save['id'])) {
return true;
}
if ($save['thold_type'] == 0) {
/**
* Hi / Low Checks
*
* Alert must always be set except when first creating a thold!
*/
if (isset($save['thold_hi'])) {
if ($save['thold_hi'] == '' && $save['thold_low'] == '') {
$banner .= ($banner != '' ? '<br>' : '') . __('You must specify either \'High Alert Threshold\' or \'Low Alert Threshold\' or both!', 'thold');
$_SESSION['sess_error_fields']['thold_hi'] = 'thold_hi';
$_SESSION['sess_error_fields']['thold_low'] = 'thold_low';
}
if ($save['thold_hi'] != '' && $save['thold_low'] != '' && $save['thold_low'] >= $save['thold_hi']) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: \'High Alert Threshold\' smaller than the \'Low Alert Threshold\'!', 'thold');
$_SESSION['sess_error_fields']['thold_hi'] = 'thold_hi';
$_SESSION['sess_error_fields']['thold_low'] = 'thold_low';
}
// Warning is optional. Check if set!
if ($save['thold_warning_hi'] != '' || $save['thold_warning_low'] != '') {
if ($save['thold_warning_hi'] != '' && $save['thold_warning_low'] != '' && $save['thold_warning_low'] >= $save['thold_warning_hi']) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: \'High Warning Threshold\' smaller than the \'Low Warning Threshold\'!', 'thold');
$_SESSION['sess_error_fields']['thold_warning_hi'] = 'thold_warning_hi';
$_SESSION['sess_error_fields']['thold_warning_low'] = 'thold_warning_low';
}
if (!empty($save['thold_warning_hi']) && $save['thold_hi'] <= $save['thold_warning_hi']) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: \'High Warning Threshold\' larger than the \'High Alert Threshold\'!', 'thold');
$_SESSION['sess_error_fields']['thold_hi'] = 'thold_hi';
$_SESSION['sess_error_fields']['thold_warning_hi'] = 'thold_warning_hi';
}
if (!empty($save['thold_warning_low']) && $save['thold_low'] >= $save['thold_warning_low']) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: \'Low Alert Threshold\' larger than the \'Low Warning Threshold\'!', 'thold');
$_SESSION['sess_error_fields']['thold_low'] = 'thold_low';
$_SESSION['sess_error_fields']['thold_warning_low'] = 'thold_warning_low';
}
}
}
} elseif ($save['thold_type'] == 1) {
/**
* Baseline Deviation Checks
*
* The initial template creation is always hi/low. So, we
* do not require an isset in this section.
*/
if (empty($save['bl_ref_time_range']) || $save['bl_ref_time_range'] <= 0) {
$banner .= ($banner != '' ? '<br>' : '') . __('Time reference in the past must be set to positive integer value!', 'thold');
$_SESSION['sess_error_fields']['bl_ref_time_range'] = 'bl_ref_time_range';
}
if (empty($save['bl_pct_down']) && empty($save['bl_pct_up'])) {
$banner .= ($banner != '' ? '<br>' : '') . __('You must specify either \'Baseline Deviation UP\' or \'Baseline Deviation DOWN\' or both!', 'thold');
$_SESSION['sess_error_fields']['bl_pct_up'] = 'bl_pct_up';
$_SESSION['sess_error_fields']['bl_pct_down'] = 'bl_pct_down';
}
} elseif ($save['thold_type'] == 2) {
/**
* Time Based Checks
*
* The initial template creation is always hi/low. So, we
* do not require an isset in this section.
*/
if ($save['time_hi'] == '' && $save['time_low'] == '') {
$banner .= ($banner != '' ? '<br>' : '') . __('You must specify either \'High Alert Threshold\' or \'Low Alert Threshold\' or both!', 'thold');
$_SESSION['sess_error_fields']['time_hi'] = 'time_hi';
$_SESSION['sess_error_fields']['time_low'] = 'time_low';
}
if ($save['time_hi'] != '' && $save['time_low'] != '' && $save['time_low'] >= $save['time_hi']) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: \'High Alert Threshold\' smaller than the \'Low Alert Threshold\'', 'thold');
$_SESSION['sess_error_fields']['time_hi'] = 'time_hi';
$_SESSION['sess_error_fields']['time_low'] = 'time_low';
}
if (empty($save['time_fail_trigger']) || $save['time_fail_trigger'] <= 0) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: The Alert Trigger Count must be greater or eval to 1!', 'thold');
$_SESSION['sess_error_fields']['time_fail_trigger'] = 'time_fail_trigger';
}
if ($save['time_fail_length'] < $save['time_fail_trigger']) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: The Alert Trigger Count must be less than or equal to the Trigger Count or eval to 1!', 'thold');
$_SESSION['sess_error_fields']['time_fail_length'] = 'time_fail_length';
$_SESSION['sess_error_fields']['time_fail_trigger'] = 'time_fail_trigger';
}
// Warning is optional. Check if set!
if ($save['time_warning_hi'] != '' || $save['time_warning_low'] != '') {
if ($save['time_warning_hi'] != '' && $save['time_warning_low'] != '' && $save['time_warning_low'] >= $save['time_warning_hi']) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: \'High Warning Threshold\' smaller than the \'Low Warning Threshold\'!', 'thold');
$_SESSION['sess_error_fields']['time_warning_hi'] = 'time_warning_hi';
$_SESSION['sess_error_fields']['time_warning_low'] = 'time_warning_low';
}
if (empty($save['time_warning_fail_trigger']) || $save['time_warning_fail_trigger'] <= 0) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: The Warning Trigger Count must be greater or eval to 1!', 'thold');
$_SESSION['sess_error_fields']['time_warning_fail_trigger'] = 'time_warning_fail_trigger';
}
if ($save['time_warning_fail_length'] < $save['time_warning_fail_trigger']) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: The Warning Trigger Count must be less than or equal to the Trigger Count or eval to 1!', 'thold');
$_SESSION['sess_error_fields']['time_warning_fail_trigger'] = 'time_warning_fail_trigger';
$_SESSION['sess_error_fields']['time_warning_fail_length'] = 'time_warning_fail_length';
}
if (!empty($save['time_warning_hi']) && $save['time_hi'] <= $save['time_warning_hi']) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: \'High Warning Threshold\' larger than the \'High Alert Threshold\'!', 'thold');
$_SESSION['sess_error_fields']['time_hi'] = 'time_hi';
$_SESSION['sess_error_fields']['time_warning_hi'] = 'time_warning_hi';
}
if (!empty($save['time_warning_low']) && $save['time_low'] >= $save['time_warning_low']) {
$banner .= ($banner != '' ? '<br>' : '') . __('Impossible threshold: \'Low Warning Threshold\' smaller than the \'Low Alert Threshold\'!', 'thold');
$_SESSION['sess_error_fields']['time_low'] = 'time_low';
$_SESSION['sess_error_fields']['time_warning_low'] = 'time_warning_low';
}
}
} else {
$banner .= ($banner != '' ? '<br>' : '') . __('Unknown Threshold Type!', 'thold');
}
if ($banner != '') {
thold_raise_message($banner, MESSAGE_LEVEL_ERROR);
return false;
}
return true;
}
// populate the save structure from a thold template
/**
* thold_create_thold_save_from_template - copies a threshold template's settings (bounds,
* triggers, notification, CDEF, syslog, commands, acknowledgment, subjects/bodies, SNMP, notes)
* into a threshold save array as the starting point for a template-derived threshold.
*
* @param array $save The in-progress threshold save array to populate.
* @param array $template The threshold template row to copy settings from.
*
* @return array The populated threshold save array.
*/
function thold_create_thold_save_from_template($save, $template) {
// General Settings
$save['name'] = $template['suggested_name'];
$save['data_template_id'] = $template['data_template_id'];
$save['data_source_name'] = $template['data_source_name'];
$save['thold_template_id'] = $template['id'];
$save['template_enabled'] = 'on';
$save['thold_enabled'] = $template['thold_enabled'];
$save['thold_type'] = $template['thold_type'];
// Additional General
$save['thold_alert'] = 0;
$save['restored_alert'] = $template['restored_alert'];
$save['repeat_alert'] = $template['repeat_alert'];
$save['exempt'] = $template['exempt'];
// Alert High/Low
$save['thold_hi'] = $template['thold_hi'];
$save['thold_low'] = $template['thold_low'];
$save['thold_fail_trigger'] = $template['thold_fail_trigger'];
// Warning High/Low
$save['thold_warning_hi'] = $template['thold_warning_hi'];
$save['thold_warning_low'] = $template['thold_warning_low'];
$save['thold_warning_fail_trigger'] = $template['thold_warning_fail_trigger'];
$save['thold_warning_fail_count'] = $template['thold_warning_fail_count'];
// Alert Time Based
$save['time_hi'] = $template['time_hi'];
$save['time_low'] = $template['time_low'];
$save['time_fail_trigger'] = $template['time_fail_trigger'];
$save['time_fail_length'] = $template['time_fail_length'];
// Warning Time Based
$save['time_warning_hi'] = $template['time_warning_hi'];
$save['time_warning_low'] = $template['time_warning_low'];
$save['time_warning_fail_trigger'] = $template['time_warning_fail_trigger'];
$save['time_warning_fail_length'] = $template['time_warning_fail_length'];
// Baseline
$save['bl_type'] = $template['bl_type'];
$save['bl_cf'] = $template['bl_cf'];
$save['bl_ref_time_range'] = $template['bl_ref_time_range'];
$save['bl_pct_down'] = $template['bl_pct_down'];
$save['bl_pct_up'] = $template['bl_pct_up'];
$save['bl_fail_trigger'] = $template['bl_fail_trigger'];
$save['bl_fail_count'] = $template['bl_fail_count'];
$save['bl_alert'] = $template['bl_alert'];
// Notification
$save['notify_alert'] = $template['notify_alert'];
$save['notify_warning'] = $template['notify_warning'];
$save['notify_extra'] = $template['notify_extra'];
$save['notify_warning_extra'] = $template['notify_warning_extra'];
// Data Manipulation
$save['data_type'] = $template['data_type'];
$save['cdef'] = $template['cdef'];
$save['percent_ds'] = $template['percent_ds'];
$save['expression'] = $template['expression'];
$save['upper_ds'] = $template['upper_ds'];
$save['decimals'] = $template['decimals'];
$save['units_suffix'] = $template['units_suffix'];
$save['show_units'] = $template['show_units'];
// Hrules
$save['thold_hrule_alert'] = $template['thold_hrule_alert'];
$save['thold_hrule_warning'] = $template['thold_hrule_warning'];
$save['skipscale'] = $template['skipscale'];
// Syslog
$save['syslog_enabled'] = $template['syslog_enabled'];
$save['syslog_priority'] = $template['syslog_priority'];
$save['syslog_facility'] = $template['syslog_facility'];
// Command execution
$save['trigger_cmd_high'] = $template['trigger_cmd_high'];
$save['trigger_cmd_low'] = $template['trigger_cmd_low'];
$save['trigger_cmd_norm'] = $template['trigger_cmd_norm'];
// Acknowledgment
$save['reset_ack'] = $template['reset_ack'];
$save['persist_ack'] = $template['persist_ack'];
// Email Subjects
$save['email_subject'] = $template['email_subject'];
$save['email_subject_warn'] = $template['email_subject_warn'];
$save['email_subject_restoral'] = $template['email_subject_restoral'];
// Email Bodies
$save['email_body'] = $template['email_body'];
$save['email_body_warn'] = $template['email_body_warn'];
$save['email_body_restoral'] = $template['email_body_restoral'];
// SNMP
$save['snmp_event_category'] = $template['snmp_event_category'];
$save['snmp_event_description'] = $template['snmp_event_description'];
$save['snmp_event_severity'] = $template['snmp_event_severity'];
$save['snmp_event_warning_severity'] = $template['snmp_event_warning_severity'];
// Other
$save['notes'] = $template['notes'];
$save['external_id'] = $template['external_id'];
$save['format_file'] = $template['format_file'];
$save['graph_timespan'] = $template['graph_timespan'];
return $save;
}
// Create tholds for all possible data elements for a host
/**
* autocreate - creates thresholds from their associated templates for every eligible data source
* on the given device(s)/graph(s)/graph template/threshold template, skipping data sources that
* already have a threshold from that template.
*
* @param int|array $device_ids A single device id, or an array of device ids to scope to.
* @param array|string $graph_ids Optional array of local graph ids to further scope to.
* @param int|string $graph_template_id Optional graph template id to further scope to.
* @param int|string $thold_template_id Optional threshold template id to restrict creation to.
* @param bool $log Whether to raise a UI message when no matching templates are found for a
* single device.
*
* @return int The number of thresholds created.
*/
function autocreate($device_ids, $graph_ids = '', $graph_template_id = '', $thold_template_id = '', $log = false) {
$created = 0;
$message = '';
$device_id = 0;
// Don't autocreate if not asked to
if (isset_request_var('save_autocreate') && get_filter_request_var('save_autocreate') == 0) {
return;
}
$sql_where = '';
if (!empty($thold_template_id) && is_numeric($thold_template_id)) {
$sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . 'tt.id = ' . $thold_template_id;
if (empty($graph_template_id)) {
$data_template_id = db_fetch_cell_prepared('SELECT data_template_id
FROM thold_template
WHERE id = ?',
[$thold_template_id]);
if ($data_template_id > 0) {
$graph_template_ids = array_rekey(
db_fetch_assoc_prepared('SELECT DISTINCT gti.graph_template_id
FROM graph_templates_item AS gti
INNER JOIN data_template_rrd AS dtr
ON gti.task_item_id = dtr.id
AND gti.local_graph_id = 0
AND dtr.data_template_id = ?',
[$data_template_id]),
'graph_template_id', 'graph_template_id'
);
if (cacti_sizeof($graph_template_ids)) {
$sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . 'gl.graph_template_id IN (' . implode(', ', $graph_template_ids) . ')';
}
}
}
}
if (!empty($graph_template_id) && is_numeric($graph_template_id)) {
$sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . 'gl.graph_template_id = ' . $graph_template_id;
}
if (is_array($graph_ids) && cacti_sizeof($graph_ids)) {
$sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . 'gti.local_graph_id IN(' . implode(', ', $graph_ids) . ')';
}
if (is_array($device_ids)) {
$sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . 'gl.host_id IN(' . implode(', ', array_map('intval', $device_ids)) . ')';
} elseif ($device_ids > 0) {
$device_id = $device_ids;
}
if ($device_id > 0) {
$host_template_id = db_fetch_cell_prepared('SELECT host_template_id
FROM host
WHERE id = ?',
[$device_id]);
$templates = db_fetch_assoc_prepared('SELECT tt.*
FROM thold_template AS tt
INNER JOIN plugin_thold_host AS pth
ON tt.id = pth.thold_template_id
WHERE pth.host_id = ?',
[$device_id]);
if (!cacti_sizeof($templates)) {
if ($log) {
thold_raise_message(__('No Thresholds Templates associated with the Device\'s Template.', 'thold'), MESSAGE_LEVEL_ERROR);
}
return 0;
} else {
$sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . 'gl.host_id = ' . $device_id;
$sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . 'dtr.data_source_name = ?';
foreach ($templates as $template) {
$new_where = $sql_where . ' AND tt.id = ' . $template['id'];
$data_sources = db_fetch_assoc_prepared("SELECT DISTINCT
dtr.id, gti.local_graph_id, local_data_id, gl.snmp_query_id
FROM data_template_rrd AS dtr
INNER JOIN thold_template AS tt
ON tt.data_template_id = dtr.data_template_id
AND tt.data_source_name = dtr.data_source_name
INNER JOIN graph_templates_item AS gti
ON gti.task_item_id = dtr.id
INNER JOIN graph_local AS gl
ON gl.id = gti.local_graph_id
$new_where",
[$template['data_source_name']]);
if (cacti_sizeof($data_sources)) {
foreach ($data_sources as $data_source) {
$local_data_id = $data_source['local_data_id'];
$local_graph_id = $data_source['local_graph_id'];
$data_template_rrd_id = $data_source['id'];
// Don't create a second threshold for a data source that already has a threshold
if ($data_source['snmp_query_id'] > 0) {
$exists = db_fetch_cell_prepared('SELECT id
FROM thold_data
WHERE local_data_id = ?
AND thold_template_id = ?
AND data_template_rrd_id = ?',
[$local_data_id, $template['id'], $data_template_rrd_id]);
} else {
$exists = false;
}
if (!$exists && thold_create_from_template($local_data_id, $local_graph_id, $data_template_rrd_id, $template, $message)) {
$created++;
}
}
}
}
}
} else {
$data_sources = db_fetch_assoc("SELECT DISTINCT
dtr.id, gl.id AS local_graph_id, local_data_id, tt.id AS thold_template_id, gl.snmp_query_id
FROM data_template_rrd AS dtr
INNER JOIN thold_template AS tt
ON tt.data_template_id = dtr.data_template_id
AND tt.data_source_name = dtr.data_source_name
INNER JOIN graph_templates_item AS gti
ON gti.task_item_id = dtr.id
INNER JOIN graph_local AS gl
ON gl.id = gti.local_graph_id
$sql_where");
if (cacti_sizeof($data_sources)) {
foreach ($data_sources as $data_source) {
$local_data_id = $data_source['local_data_id'];
$local_graph_id = $data_source['local_graph_id'];
$data_template_rrd_id = $data_source['id'];
$template = db_fetch_row_prepared('SELECT *
FROM thold_template
WHERE id = ?',
[$data_source['thold_template_id']]);
if (cacti_sizeof($template)) {
foreach ($data_sources as $inner_data_source) {
// Don't create a second threshold for a data source that already has a threshold
if ($inner_data_source['snmp_query_id'] > 0) {
$exists = db_fetch_cell_prepared('SELECT id
FROM thold_data
WHERE local_data_id = ?
AND thold_template_id = ?
AND data_template_rrd_id = ?',
[$local_data_id, $template['id'], $data_template_rrd_id]);
} else {
$exists = false;
}
if (!$exists && thold_create_from_template($local_data_id, $local_graph_id, $data_template_rrd_id, $template, $message)) {
$created++;
}
}
}
}
}
}
if (strlen($message)) {
thold_raise_message($message, MESSAGE_LEVEL_INFO);
} else {
thold_raise_message(__('No Threshold(s) Created. Either they already exist, or no suitable matches found.', 'thold'), MESSAGE_LEVEL_INFO);
}
return $created;
}
/**
* thold_create_from_template - creates a single threshold for a data source from a threshold
* template, provided the data source name matches the template and no matching threshold already
* exists.
*
* @param int $local_data_id The local data source id to create the threshold for.
* @param int $local_graph_id The local graph id the data source belongs to.
* @param int $data_template_rrd_id The data_template_rrd id backing the data source.
* @param array|int $template_or_id The threshold template row, or its id to look one up.
* @param string &$message Appended with a created-threshold summary line on success.
*
* @return bool True when a new threshold was created; false when it already existed, the data
* source didn't match the template, or the save failed validation.
*/
function thold_create_from_template($local_data_id, $local_graph_id, $data_template_rrd_id, $template_or_id, &$message) {
if (is_array($template_or_id)) {
$template = $template_or_id;
} else {
$template = db_fetch_row_prepared('SELECT *
FROM thold_template
WHERE id = ?',
[$template_or_id]);
}
$data_source_name = db_fetch_cell_prepared('SELECT data_source_name
FROM data_template_rrd
WHERE id = ?',
[$data_template_rrd_id]);
// Don't create the threshold if the dtr data source
// does not match the templates
if ($data_source_name != $template['data_source_name']) {
return false;
}
$exists = db_fetch_cell_prepared('SELECT id
FROM thold_data
WHERE local_graph_id = ?
AND local_data_id = ?
AND data_template_rrd_id = ?
AND data_source_name = ?
AND thold_template_id = ?',
[
$local_graph_id,
$local_data_id,
$data_template_rrd_id,
$template['data_source_name'],
$template['id']
]
);
if (!$exists) {
$graph = db_fetch_row_prepared('SELECT gtg.local_graph_id, gtg.title_cache,
gtg.graph_template_id, gl.host_id
FROM graph_templates_graph AS gtg
INNER JOIN graph_local AS gl
ON gl.id = gtg.local_graph_id
WHERE gtg.local_graph_id = ?',
[$local_graph_id]);
if (cacti_sizeof($template) && cacti_sizeof($graph)) {
$save = [];
$save['id'] = 0;
$save['local_data_id'] = $local_data_id;
$save['data_template_rrd_id'] = $data_template_rrd_id;
$save['local_graph_id'] = $graph['local_graph_id'];
$save['graph_template_id'] = $graph['graph_template_id'];
$save['host_id'] = $graph['host_id'];
$save = thold_create_thold_save_from_template($save, $template);
$save['name_cache'] = thold_expand_string($save, $save['name']);
$save = api_plugin_hook_function('thold_edit_save_thold', $save);
if (!thold_validate_save($save, 'thold')) {
header('Location: thold.php?action=edit&header=false&id=' . ($save['id'] > 0 ? $save['id'] : ''));
exit;
}
$id = sql_save($save, 'thold_data');
set_config_option('time_last_change_thold', time());
if ($id) {
thold_template_update_threshold($id, $save['thold_template_id']);
plugin_thold_log_changes($id, 'auto_created', $save['name_cache']);
$message .= __esc('Created Threshold: %s', $save['name_cache'], 'thold') . '<br>';
return true;
}
}
}
return false;
}
// Sends a group of graphs to a user
/**
* thold_mail - sends (or, when the notification queue is enabled, enqueues) a threshold/device
* notification email, attaching graph images referenced by `<GRAPH>` in the message body and
* applying any configured report format file.
*
* @param string $to_email Comma-separated recipient addresses.
* @param string $bcc_email Comma-separated blind-copy addresses.
* @param string $from_email Sender address; falls back to the 'thold_from_email' setting (or a
* generated cacti@hostname address) when empty.
* @param string $subject The message subject line.
* @param string $message The message body (may contain a `<GRAPH>`/`<SUBJECT>` token).
* @param array $filename Graph attachment descriptor(s) (local_graph_id/local_data_id), or a
* single descriptor array.
* @param array $headers Additional mail headers to merge in.
* @param int $notify_list_id The notification list id this send belongs to, when queuing.
* @param array &$host The device row associated with this notification.
* @param string $format_file Optional report format file to wrap the message body in.
* @param int $graph_timespan The graph attachment's timespan, in days.
* @param string $topic The notification_queue topic to use when queuing.
*
* @global array $config Cacti's global configuration array.
*
* @return string An empty string on success (or successful queuing), or the mailer/validation
* error message on failure.
*/
function thold_mail($to_email, $bcc_email, $from_email, $subject, $message, $filename, $headers = [], $notify_list_id = 0, &$host = [], $format_file = '', $graph_timespan = 7, $topic = 'thold_mail') {
thold_debug('Preparing to send email');
global $config;
$subject = trim($subject);
$message = thold_str_replace('<SUBJECT>', $subject, $message);
if ($from_email == '') {
$from_email = read_config_option('thold_from_email');
$from_name = read_config_option('thold_from_name');
if ($from_email == '') {
if (isset($_SERVER['HOSTNAME'])) {
$from_email = 'cacti@' . $_SERVER['HOSTNAME'];
} else {
$from_email = 'cacti@' . gethostname();
}
}
if ($from_name == '') {
$from_name = 'Cacti';
}
}
if ($to_email == '') {
return __('Mailer Error: No <b>TO</b> address set!!<br>If using the <i>Test Mail</i> link, please set the <b>Alert Email</b> setting.', 'thold');
}
$attachments = [];
$notification_queue = read_config_option('thold_notification_queue');
if (cacti_sizeof($filename) && strstr($message, '<GRAPH>') !== false) {
if (isset($filename['local_data_id'])) {
$tmp = [];
$tmp[] = $filename;
$filename = $tmp;
}
$time = time();
// get config option for first-day-of-the-week
$first_weekdayid = read_user_setting('first_weekdayid');
$timespan = [];
// get start/end time-since-epoch for actual time (now()) and given current-session-timespan
get_timespan($timespan, $time, $graph_timespan, $first_weekdayid);
foreach ($filename as $val) {
$graph_data_array = [
'graph_start' => $timespan['begin_now'],
'graph_end' => $timespan['end_now'],
'image_format' => 'png',
'graph_theme' => 'modern',
'output_flag' => RRDTOOL_OUTPUT_STDOUT,
'disable_cache' => true
];
if ($notification_queue == 'on') {
$attach = base64_encode(rrdtool_function_graph($val['local_graph_id'], '', $graph_data_array, ''));
} else {
$attach = rrdtool_function_graph($val['local_graph_id'], '', $graph_data_array, '');
}
$attachments[] = [
'attachment' => $attach,
'filename' => 'graph_' . $val['local_graph_id'] . '.png',
'mime_type' => 'image/png',
'local_graph_id' => $val['local_graph_id'],
'local_data_id' => $val['local_data_id'],
'inline' => 'inline'
];
}
}
$text = ['text' => '', 'html' => ''];
if (empty($filename)) {
$text['html'] = $message . '<br>';
$message = str_replace('<br>', "\n", $message);
$message = str_replace('<BR>', "\n", $message);
$message = str_replace('</BR>', "\n", $message);
$text['text'] = strip_tags(str_replace('<br>', "\n", $message));
} else {
$text['html'] = $message . '<br>';
$text['text'] = strip_tags(str_replace('<br>', "\n", $message));
}
$format_data = '';
$report_tag = false;
$format_ok = false;
$theme = read_config_option('selected_theme');
if ($theme == '') {
$theme = 'classic';
}
// process the format file as applicable
if ($format_file != '') {
include_once($config['library_path'] . '/reports.php');
$format_ok = reports_load_format_file($format_file, $format_data, $report_tag, $theme);
}
if (($format_ok && $report_tag) || strpos($text['html'], '<body>') !== false) {
$include_body = false;
} else {
$include_body = true;
}
if ($include_body) {
$text['html'] = '<body>' . $text['html'] . '</body>';
}
if ($format_ok) {
if ($report_tag) {
$text['html'] = str_replace('<REPORT>', $text['html'], $format_data);
} else {
$text['html'] = $format_data . PHP_EOL . $text['html'];
}
}
$version = db_fetch_cell("SELECT version
FROM plugin_config
WHERE directory='thold'");
if (!is_array($headers)) {
$headers = [];
}
$headers['X-Mailer'] = 'Cacti-Thold-v' . $version;
$headers['User-Agent'] = 'Cacti-Thold-v' . $version;
if (read_config_option('thold_email_prio') == 'on') {
$headers['X-Priority'] = '1';
}
thold_debug("Queuing email to '" . trim($to_email,', ') . "'");
$thold_send_text_only = read_config_option('thold_send_text_only');
$thold_send_email_separately = read_config_option('thold_send_email_separately');
$any_error = '';
if ($thold_send_email_separately != 'on') {
if ($notification_queue == 'on') {
$data = [
'from' => [$from_email, $from_name],
'to' => $to_email,
'bcc' => $bcc_email,
'subject' => $subject,
'body' => $text['html'],
'body_text' => $text['text'],
'attachments' => empty($attachments) ? null : $attachments,
'headers' => $headers,
'html' => $thold_send_text_only != 'on',
'id' => (isset($host['thold_id']) ? $host['thold_id'] : $host['id'])
];
thold_notification_add($topic, $data, 'id', $notify_list_id, $host);
} else {
$any_error = mailer(
[$from_email, $from_name],
$to_email,
'',
$bcc_email,
'',
$subject,
$text['html'],
$text['text'],
empty($attachments) ? null : $attachments,
$headers,
$thold_send_text_only != 'on'
);
}
} else {
$ar_to_email = explode(',', $to_email);
foreach ($ar_to_email as $to) {
if (filter_var($to, FILTER_VALIDATE_EMAIL) == $to) {
if ($notification_queue == 'on') {
$data = [
'from' => [$from_email, $from_name],
'to' => $to,
'bcc' => $bcc_email,
'subject' => $subject,
'body' => $text['html'],
'body_text' => $text['text'],
'attachments' => empty($attachments) ? '' : $attachments,
'headers' => $headers,
'html' => $thold_send_text_only != 'on',
'id' => (isset($host['thold_id']) ? $host['thold_id'] : $host['id'])
];
thold_notification_add($topic, $data, 'id', $notify_list_id, $host);
} else {
$error = mailer(
[$from_email, $from_name],
$to,
'',
'',
'',
$subject,
$text['html'],
$text['text'],
empty($attachments) ? '' : $attachments,
$headers,
$thold_send_text_only != 'on'
);
if ($error != '') {
cacti_log("ERROR: Sending Email Failed To:$to Subject:$subject. Error was:'$error'", true, 'THOLD');
$any_error = $error;
}
}
}
}
}
if ($any_error != '') {
return $any_error;
}
return '';
}
/**
* thold_notification_add - enqueues one notification (email/command/etc.) into the notification_queue
* table for deferred delivery, resolving a display name and originating device from the payload.
*
* @param string $topic The notification_queue topic identifying the delivery type.
* @param array &$data The notification payload to persist (JSON-encoded).
* @param string $id The key within $data['data'] holding the source object's id.
* @param int $list_id The notification list id this entry belongs to.
* @param array &$host The originating device row, if any.
*
* @return void
*/
function thold_notification_add($topic, &$data, $id = 'id', $list_id = 0, &$host = []) {
$now = date('Y-m-d H:i:s');
if (isset($data['data'][$id])) {
$id = $data['data'][$id];
} else {
$id = 0;
}
if (isset($data['data']['name'])) {
$name = $data['data']['name'];
} elseif (isset($data['data']['description'])) {
$name = $data['data']['description'];
} elseif (isset($data['subject'])) {
$name = $data['subject'];
} else {
$name = 'Not Specfified';
}
if (isset($host['id'])) {
$host_id = $host['id'];
} else {
$host_id = 0;
}
if (isset($host['hostname'])) {
if ($host['hostname'] == 'localhost') {
$hostname = $host['description'];
} else {
$hostname = $host['hostname'];
}
} else {
$hostname = 'unknown';
}
db_execute_prepared('INSERT INTO notification_queue
(topic, notification_list_id, object_id, object_name, host_id, hostname, event_time, event_data) VALUES
(?, ?, ?, ?, ?, ?, ?, ?)',
[$topic, $list_id, $id, $name, $host_id, $hostname, $now, json_encode($data, JSON_PRETTY_PRINT)]);
}
/**
* pre_process_device_notifications - implements device-down notification storm suppression:
* evaluates the 'alert_notification_pause' rule set (global event count, per-site count, percent
* of a site's hosts, or percent of all hosts) against unprocessed device-down queue entries since
* the last check, then delegates to check_for_expired_delays()/check_for_new_delays() to send
* final notifications for expired delay windows and start delay windows for newly-triggered rules.
*
* @return void
*/
function pre_process_device_notifications() {
$delay_criteria = read_config_option('alert_notification_pause');
$now = time();
$triggers = [];
$last_check = read_config_option('alert_deadnotify_last_check');
$last_trigger = read_config_option('alert_deadnotify_state');
if ($last_trigger != '') {
$last_trigger = json_decode($last_trigger, true);
}
if ($delay_criteria != '') {
$options = explode(',', $delay_criteria);
foreach ($options as $option) {
$value_array = explode('|', $option);
if (isset($value_array[0])) {
$value = $value_array[0];
} else {
$value = '';
}
if (isset($value_array[1])) {
$type = $value_array[1];
} else {
$type = '';
}
switch($type) {
case 'eg': // Global events
$events = db_fetch_cell_prepared('SELECT COUNT(*) AS events
FROM notification_queue
WHERE event_processed = 0
AND topic IN ("thold_dhost_mail", "thold_dhost_cmd")
AND event_time BETWEEN ? AND ?',
[
date('Y-m-d H:i:s', $last_check),
date('Y-m-d H:i:s', $now)
]
);
if ($events > $value) {
$triggers[$option] = [
'events' => $events,
'time' => $now
];
}
break;
case 'es': // Events per Site
$events = db_fetch_assoc_prepared('SELECT site_id, COUNT(*) AS events
FROM notification_queue AS nq
INNER JOIN host AS h
ON nq.host_id = h.id
WHERE event_processed = 0
AND topic IN ("thold_dhost_mail", "thold_dhost_cmd")
AND event_time BETWEEN ? AND ?
GROUP BY site_id
HAVING events > ?',
[
date('Y-m-d H:i:s', $last_check),
date('Y-m-d H:i:s', $now),
$value
]
);
if (cacti_sizeof($events)) {
foreach ($events as $e) {
$triggers[$option . '|' . $e['site_id']] = [
'events' => $e['events'],
'time' => $now
];
}
}
break;
case 'pes': // Percent of events per Site
$events = db_fetch_assoc_prepared('SELECT h.site_id, COUNT(*) AS events, th.total_hosts
FROM notification_queue AS nq
INNER JOIN host AS h
ON nq.host_id = h.id
INNER JOIN (
SELECT site_id, COUNT(*) AS total_hosts
FROM host
WHERE disabled = ""
AND deleted = ""
GROUP BY site_id
) AS th
ON th.site_id = h.site_id
WHERE nq.event_processed = 0
AND nq.topic IN ("thold_dhost_mail", "thold_dhost_cmd")
AND nq.event_time BETWEEN ? AND ?
GROUP BY h.site_id
HAVING (events / total_hosts) * 100 > ?',
[
date('Y-m-d H:i:s', $last_check),
date('Y-m-d H:i:s', $now),
$value
]
);
if (cacti_sizeof($events)) {
foreach ($events as $e) {
$triggers[$option . '|' . $e['site_id']] = [
'events' => $e['events'],
'time' => $now
];
}
}
break;
case 'peg': // Percent of events Globally
$events = db_fetch_row_prepared('SELECT
(SELECT COUNT(*) FROM host WHERE disabled = "" AND deleted = "") AS total_hosts,
COUNT(nq.host_id) AS events
FROM notification_queue AS nq
INNER JOIN host AS h
ON h.id = nq.host_id
WHERE h.disabled = ""
AND h.deleted = ""
AND nq.event_processed = 0
AND nq.topic IN ("thold_dhost_mail", "thold_dhost_cmd")
AND nq.event_time BETWEEN ? AND ?
HAVING (events / total_hosts) * 100 > ?',
[
date('Y-m-d H:i:s', $last_check),
date('Y-m-d H:i:s', $now),
$value
]
);
if (cacti_sizeof($events)) {
$triggers[$option] = [
'events' => $events['events'],
'time' => $now
];
}
break;
default:
$events = [];
$triggers = [];
break;
}
}
}
/**
* Review the old triggers first to see if the trigger period is over
* and it's time to provide final notification and communication
*
* This function will provide initial notification to the notification
* list associated with the devices of the initial event.
*/
check_for_expired_delays($last_trigger, $triggers, $now, $last_check);
/**
* Review the new triggers now to see if there are any new triggers
* and to setup notification delay for them. The prior step will
* unset any active triggers leaving only new triggers to setup
* and deal with.
*
* This function will also provide any final notification and set the
* JSON config option for future notification delay actions
*/
check_for_new_delays($last_trigger, $triggers, $now, $last_check);
}
/**
* check_for_expired_delays - This function will look at the start time of
* any trigger delays, and if the delay time has expired, it will check
* the status of the hosts in question, and if they are still in a down
* state, a final notification will be sent via Email and or Command with
*
* @param array - Any active triggers that may or may not remain active
* @param array - Any new triggers that may be part of an existing delay
* or a new delay based upon the ruleset.
* @param int - The Unix Timestamp of the start time for the sample period
* @param int - The Unix Timestamp of the last time that a check was performed.
* @param mixed $last_trigger
* @param mixed $triggers
* @param mixed $now
* @param mixed $last_check
*
* @return null - Though the two trigger variables that were passed by reference
* will be modified based upon the findings. For example, if a
* delay has expired and we provided notification the delay will
* be unset, also if we find additional events for and existing
* delay, it will be removed from the new events.
*/
function check_for_expired_delays(&$last_trigger, $triggers, $now, $last_check) {
$delay_period = read_config_option('alert_notification_delay');
if (cacti_sizeof($last_trigger)) {
foreach ($last_trigger as $type => $details) {
$parts = explode('|', $type);
/**
* Its time to provide the final notification for this notification
* delay. Check the type, the variout notification lists and the
* status of the hosts. If they are no longer down, you can cancel
* any notifications if not and send out an all clear event.
* Otherwise, trigger the notifications as directed.
*/
if ($now - $details['start_time'] >= $delay_period) {
switch($parts[0]) {
case 'eg':
case 'peg':
$hosts_down = db_fetch_assoc_prepared('SELECT h.*,
nq.id AS notification_id, nq.notification_list_id, nq.topic,
nq.event_data, nq.event_time
FROM host AS h
INNER JOIN notification_queue AS nq
ON h.id = nq.host_id
WHERE nq.event_processed = 0
AND nq.topic IN ("thold_dhost_mail", "thold_dhost_cmd")
AND nq.event_time BETWEEN ? AND ?
AND h.status NOT IN (?, ?)',
[
date('Y-m-d H:i:s', $details['start_time']),
date('Y-m-d H:i:s', $now),
HOST_RECOVERING,
HOST_UP
]
);
if (cacti_sizeof($hosts_down)) {
// perform notification of errors
$hosts = array_rekey($hosts_down, 'id', 'id');
} else {
// perform notification of clearing of errors
}
if (isset($triggers[$type])) {
unset($triggers[$type]);
}
break;
case 'es':
case 'pes':
$hosts_down = db_fetch_assoc_prepared('SELECT h.*,
nq.id AS notification_id, nq.notification_list_id, nq.topic,
nq.event_data, nq.event_time
FROM host AS h
INNER JOIN notification_queue AS nq
ON h.id = nq.host_id
WHERE nq.event_processed = 0
AND h.site_id = ?
AND nq.topic IN ("thold_dhost_mail", "thold_dhost_cmd")
AND nq.event_time BETWEEN ? AND ?
AND h.status NOT IN (?, ?)',
[
$parts[2],
date('Y-m-d H:i:s', $details['start_time']),
date('Y-m-d H:i:s', $now),
HOST_RECOVERING,
HOST_UP
]
);
if (cacti_sizeof($hosts_down)) {
// perform notification of errors
$hosts = array_rekey($hosts_down, 'id', 'id');
} else {
// perform notification of clearing of errors
}
if (isset($triggers[$type])) {
unset($triggers[$type]);
}
}
}
}
}
}
/**
* check_for_new_delays - Given the list of existing triggers and any
* new triggers, we will re-establish the triggers and provide the
* appropriate initial notification as required. The function in the
* end will set the config option for the next sampling interval to
* evaluate.
*
* @param array - Any active triggers that may or may not remain active
* @param array - Any new triggers that may be part of an existing delay
* or a new delay based upon the ruleset.
* @param int - The Unix Timestamp of the start time for the sample period
* @param int - The Unix Timestamp of the last time that a check was performed.
* @param mixed $last_trigger
* @param mixed $triggers
* @param mixed $now
* @param mixed $last_check
*
* @return null
*/
function check_for_new_delays($last_trigger, $triggers, $now, $last_check) {
$delay_period = read_config_option('alert_notification_delay');
}
/**
* Database advisory-lock name for one notification worker slot.
*
* @param int $thread
*
* @return string
*/
function thold_notification_lock_name($thread) {
return 'thold_notify_child_' . (int) $thread;
}
/**
* Acquire the cross-platform worker lease without waiting.
*
* @param int $thread
*
* @return bool
*/
function thold_notification_acquire_lock($thread) {
return (int) db_fetch_cell_prepared("SELECT GET_LOCK(CONCAT(DATABASE(), ':', ?), 0)", [thold_notification_lock_name($thread)]) === 1;
}
/**
* Verify that this database connection still owns the worker lease.
*
* @param int $thread
*
* @return bool
*/
function thold_notification_owns_lock($thread) {
return (int) db_fetch_cell_prepared("SELECT IS_USED_LOCK(CONCAT(DATABASE(), ':', ?)) = CONNECTION_ID()", [thold_notification_lock_name($thread)]) === 1;
}
/**
* Release the worker lease held by this database connection.
*
* @param int $thread
*
* @return bool
*/
function thold_notification_release_lock($thread) {
return (int) db_fetch_cell_prepared("SELECT RELEASE_LOCK(CONCAT(DATABASE(), ':', ?))", [thold_notification_lock_name($thread)]) === 1;
}
/**
* Probe Unix process liveness without treating a permission error as death.
*
* @param int $pid
* @param callable|null $getpgid
* @param callable|null $kill
* @param callable|null $last_error
*
* @return bool|null True when alive, false when confirmed gone, null when unknown.
*/
function thold_notification_probe_process($pid, $getpgid = null, $kill = null, $last_error = null) {
$pid = (int) $pid;
if ($pid <= 0) {
return false;
}
if ($getpgid === null && function_exists('posix_getpgid')) {
$getpgid = 'posix_getpgid';
}
if (is_callable($getpgid) && $getpgid($pid) !== false) {
return true;
}
if ($kill === null && function_exists('posix_kill')) {
$kill = 'posix_kill';
}
if (!is_callable($kill)) {
return null;
}
if ($kill($pid, 0)) {
return true;
}
if ($last_error === null && function_exists('posix_get_last_error')) {
$last_error = 'posix_get_last_error';
}
if (!is_callable($last_error)) {
return null;
}
$error = $last_error();
if ($error === 1) {
return true;
}
if ($error === 3) {
return false;
}
return null;
}
/**
* Verify that a live PID was already running when its process row was created.
*
* @param int $pid
* @param int $registered_at
* @param int $current_timestamp
* @param callable|null $elapsed Optional process-age probe used by tests.
*
* @return bool|null True for the registered process, false for a reused PID,
* or null when process identity cannot be verified.
*/
function thold_notification_process_matches($pid, $registered_at, $current_timestamp, $elapsed = null) {
$pid = (int) $pid;
$registered_at = (int) $registered_at;
$current_timestamp = (int) $current_timestamp;
if ($pid <= 0 || $registered_at <= 0 || $current_timestamp < $registered_at) {
return null;
}
$process_age = false;
if (is_callable($elapsed)) {
try {
$process_age = $elapsed($pid);
} catch (Throwable $error) {
return null;
}
} else {
$process_age = thold_notification_probe_elapsed($pid);
}
if (!is_int($process_age) || $process_age < 0) {
return null;
}
$row_age = $current_timestamp - $registered_at;
// The worker necessarily predates its database row. A younger process proves
// that the operating system reused the recorded PID.
return $process_age + 5 >= $row_age;
}
/**
* Read process age using procps seconds with a portable ps fallback.
*
* @param int $pid
* @param mixed $runner Optional command runner used by tests.
*
* @return int|false
*/
function thold_notification_probe_elapsed($pid, $runner = null) {
$pid = (int) $pid;
if ($runner === null && function_exists('exec')) {
$runner = static function ($field, $process_id) {
$output = [];
$status = 1;
exec('LC_ALL=C ps -o ' . $field . '= -p ' . (int) $process_id, $output, $status);
return [$status, $output];
};
}
if (!is_callable($runner)) {
return false;
}
foreach (['etimes', 'etime'] as $field) {
try {
[$status, $output] = $runner($field, $pid);
} catch (Throwable $error) {
return false;
}
if ((int) $status === 0 && isset($output[0])) {
$seconds = thold_notification_elapsed_seconds($output[0]);
if ($seconds !== false) {
return $seconds;
}
}
}
return false;
}
/**
* Parse procps seconds or the portable [[days-]hours:]minutes:seconds format.
*
* @param mixed $elapsed
*
* @return int|false
*/
function thold_notification_elapsed_seconds($elapsed) {
$elapsed = trim((string) $elapsed);
if (ctype_digit($elapsed)) {
return (int) $elapsed;
}
if (!preg_match('/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/D', $elapsed, $parts)) {
return false;
}
$days = isset($parts[1]) && $parts[1] !== '' ? (int) $parts[1] : 0;
$hours = isset($parts[2]) && $parts[2] !== '' ? (int) $parts[2] : 0;
$minutes = (int) $parts[3];
$seconds = (int) $parts[4];
return $days * 86400 + $hours * 3600 + $minutes * 60 + $seconds;
}
/**
* A process row is stale only after its latest heartbeat exceeds the timeout.
*
* @param array<string,mixed> $process
* @param int $timeout
*
* @return bool
*/
function thold_notification_process_is_stale(array $process, $timeout) {
$heartbeat = (int) ($process['heartbeat_at'] ?? ($process['started_at'] ?? 0));
$now = (int) ($process['current_timestamp'] ?? time());
$timeout = max(1, (int) $timeout);
return $heartbeat > 0 && $now >= $heartbeat && ($now - $heartbeat) >= $timeout;
}
/**
* Register a worker only after acquiring its database-backed lease.
*
* @param int $thread
* @param int $timeout
* @param callable|null $lock Optional lease acquisition used by tests.
* @param callable|null $probe Optional process liveness probe used by tests.
* @param callable|null $identity Optional process-start probe used by tests.
*
* @return bool
*/
function thold_notification_register_process($thread, $timeout = 3600, $lock = null, $probe = null, $identity = null) {
global $config;
$acquired = is_callable($lock) ? (bool) $lock($thread) : thold_notification_acquire_lock($thread);
if (!$acquired) {
cacti_log(sprintf('WARNING: Notification thread %s already has an active worker lease.', $thread), false, 'THOLD');
return false;
}
$process = db_fetch_row_prepared('SELECT pid,
UNIX_TIMESTAMP(started) AS started_at,
GREATEST(UNIX_TIMESTAMP(started), UNIX_TIMESTAMP(last_update)) AS heartbeat_at,
UNIX_TIMESTAMP() AS `current_timestamp`
FROM processes
WHERE tasktype = ?
AND taskname = ?
AND taskid = ?',
['thold_notify', 'child', $thread]);
if ($process === false) {
thold_notification_release_lock($thread);
return false;
}
if (!cacti_sizeof($process)) {
$registered = register_process_start('thold_notify', 'child', $thread, $timeout);
if (!$registered) {
thold_notification_release_lock($thread);
}
return $registered;
}
$running_pid = (int) ($process['pid'] ?? 0);
$running = null;
if (is_callable($probe)) {
$running = $probe($running_pid);
} elseif (($config['cacti_server_os'] ?? '') === 'unix') {
$running = thold_notification_probe_process($running_pid);
}
// The advisory lock prevents a new overlap, but it is not evidence that
// the recorded process died: a reconnect drops GET_LOCK while PHP lives.
// Confirmed death can recover immediately. Unknown liveness waits for an
// expired heartbeat. A live PID is trusted while fresh; after timeout its
// elapsed age must prove that it predates the recorded worker row.
$stale = thold_notification_process_is_stale($process, $timeout);
$match = $running === true && $stale
? thold_notification_process_matches(
$running_pid,
$process['started_at'] ?? 0,
$process['current_timestamp'] ?? time(),
$identity
)
: null;
if (($running === true && (!$stale || $match !== false)) || ($running === null && !$stale)) {
thold_notification_release_lock($thread);
cacti_log(sprintf('WARNING: Notification thread %s has an existing worker that is live or cannot be verified dead.', $thread), false, 'THOLD');
return false;
}
thold_notification_release_claim($running_pid);
unregister_process('thold_notify', 'child', $thread, $running_pid);
$registered = register_process_start('thold_notify', 'child', $thread, $timeout);
if (!$registered) {
thold_notification_release_lock($thread);
}
return $registered;
}
/**
* Claim all currently unowned queue rows for one registered worker.
*
* @param int $pid
*
* @return int
*/
function thold_notification_claim($pid) {
$pid = (int) $pid;
if ($pid <= 0) {
return 0;
}
// SIGKILL cannot run cleanup. Probe the small set of currently owned rows
// before running the recovery UPDATE; process_id > 0 can use its index and
// avoids scanning completed queue history on every poller cycle.
$orphaned = db_fetch_cell_prepared('SELECT 1
FROM notification_queue AS nq
LEFT JOIN processes AS p
ON p.pid = nq.process_id
AND p.tasktype = ?
AND p.taskname = ?
WHERE nq.process_id > 0
AND nq.event_processed = 0
AND p.pid IS NULL
LIMIT 1',
['thold_notify', 'child']);
if ($orphaned) {
db_execute_prepared('UPDATE notification_queue AS nq
LEFT JOIN processes AS p
ON p.pid = nq.process_id
AND p.tasktype = ?
AND p.taskname = ?
SET nq.process_id = 0
WHERE nq.process_id > 0
AND nq.event_processed = 0
AND p.pid IS NULL',
['thold_notify', 'child']);
}
db_execute_prepared('UPDATE notification_queue
SET process_id = ?
WHERE event_processed = 0
AND (next_attempt IS NULL OR next_attempt <= NOW())
AND process_id = 0',
[$pid]);
return db_affected_rows();
}
/**
* Release unfinished rows when a worker stops or suspends.
*
* @param int $pid
*
* @return bool
*/
function thold_notification_release_claim($pid) {
$pid = (int) $pid;
if ($pid <= 0) {
return true;
}
return db_execute_prepared('UPDATE notification_queue
SET process_id = 0
WHERE process_id = ?
AND event_processed = 0',
[$pid]);
}
/**
* Release queue and process ownership exactly once during shutdown.
*
* @param int $pid
* @param int $thread
* @param bool $registered
*
* @return bool
*/
function thold_notification_cleanup($pid, $thread, &$registered) {
if (!$registered) {
return true;
}
$released = thold_notification_release_claim($pid);
if (!$released) {
cacti_log(sprintf('WARNING: Failed to release unfinished notification claims for process %s.', $pid), false, 'THOLD');
}
unregister_process('thold_notify', 'child', $thread, $pid);
thold_notification_release_lock($thread);
$registered = false;
return $released;
}
/**
* Shutdown callback available before the CLI installs signal handlers.
*
* @return void
*/
function thold_notification_shutdown() {
global $notification_registered, $pid, $thread;
thold_notification_cleanup($pid, $thread, $notification_registered);
}
/**
* Execute a terminal queue update and prove the worker still owns every row.
*
* @param string $sql
* @param array $params
* @param array<int> $ids
* @param int $pid
*
* @return bool
*
* @throws RuntimeException When the claim was revoked before completion.
*/
function thold_notification_complete($sql, array $params, array $ids, $pid) {
$ids = array_values(array_filter(array_map('intval', $ids)));
$expected = cacti_sizeof($ids);
$updated = db_execute_prepared($sql, $params);
if (!$updated) {
$message = sprintf(
'ERROR: Notification process %s could not complete queue row(s) %s because the database update failed.',
(int) $pid,
implode(', ', $ids)
);
cacti_log($message, false, 'THOLD');
$placeholders = implode(', ', array_fill(0, $expected, '?'));
$fallback = $expected > 0 && db_execute_prepared("UPDATE notification_queue
SET error_code = 1, error_message = 'Completion update failed',
event_processed = 1, event_processed_time = NOW()
WHERE id IN ($placeholders)
AND process_id = ?", array_merge($ids, [(int) $pid]));
if ($fallback && db_affected_rows() >= $expected) {
return true;
}
throw new UnexpectedValueException($message);
}
$affected = db_affected_rows();
if ($expected === 0 || $affected < $expected) {
$message = sprintf(
'ERROR: Notification process %s lost ownership before completing queue row(s) %s.',
(int) $pid,
implode(', ', $ids)
);
cacti_log($message, false, 'THOLD');
throw new RuntimeException($message);
}
return true;
}
/**
* Bound queue error text to the notification_queue.error_message column.
*
* @param mixed $message
*
* @return string
*/
function thold_notification_error_message($message) {
$message = str_replace(["\r", "\n"], ' ', (string) $message);
if (function_exists('mb_check_encoding') && !mb_check_encoding($message, 'UTF-8')) {
$message = mb_convert_encoding($message, 'UTF-8', 'UTF-8');
}
return function_exists('mb_substr')
? mb_substr($message, 0, 128, 'UTF-8')
: substr($message, 0, 128);
}
/**
* Refresh a worker heartbeat only while this connection owns its lease.
*
* @param int $thread
*
* @return void
*
* @throws RuntimeException When the lease was lost.
*/
function thold_notification_heartbeat($thread) {
if (!thold_notification_owns_lock($thread)) {
throw new RuntimeException('Notification worker lease was lost.');
}
heartbeat_process('thold_notify', 'child', $thread);
}
/**
* Own and run one notification worker from registration through cleanup.
*
* Optional callables keep the startup ordering executable in unit tests. The
* CLI passes none of them and therefore uses the production implementations.
*
* @param int $thread
* @param int $timeout
* @param int|null $worker_pid
* @param callable|null $shutdown_registrar
* @param callable|null $register_worker
* @param callable|null $run_worker
*
* @return bool False when registration fails; true after a completed drain.
*/
function thold_notification_main($thread, $timeout = 3600, $worker_pid = null, $shutdown_registrar = null, $register_worker = null, $run_worker = null) {
global $notification_registered, $pid;
$pid = $worker_pid === null ? getmypid() : (int) $worker_pid;
$GLOBALS['thread'] = (int) $thread;
$notification_registered = true;
$shutdown_registrar = is_callable($shutdown_registrar) ? $shutdown_registrar : 'register_shutdown_function';
$register_worker = is_callable($register_worker) ? $register_worker : 'thold_notification_register_process';
$run_worker = is_callable($run_worker) ? $run_worker : 'thold_notification_run';
// Install cleanup before ownership acquisition. A signal can then release
// a lease acquired by register_worker even before that call returns.
$shutdown_registrar('thold_notification_shutdown');
$start = microtime(true);
try {
if (!$register_worker($thread, $timeout)) {
$notification_registered = false;
return false;
}
$total_rows = $run_worker($pid, 'all', static function () use ($thread) {
thold_notification_heartbeat($thread);
});
} catch (Throwable $error) {
cacti_log('ERROR: Notification worker stopped: ' . $error->getMessage(), false, 'THOLD');
return false;
} finally {
thold_notification_shutdown();
}
$end = microtime(true);
cacti_log(sprintf('THOLD NOTIFY STATS: Time:%0.2f Notifications:%s', $end - $start, $total_rows), false, 'SYSTEM');
return true;
}
/**
* Mark an unsupported queue topic terminal so it cannot poison every run.
*
* @param int $id
* @param int $pid
* @param string $topic
*
* @return bool
*/
function thold_notification_reject_unknown_topic($id, $pid, $topic) {
$message = thold_notification_error_message('Unsupported notification topic: ' . (string) $topic);
return thold_notification_complete('UPDATE notification_queue
SET error_code = 1, error_message = ?, event_processed = 1,
event_processed_time = NOW()
WHERE id = ?
AND process_id = ?',
[$message, (int) $id, (int) $pid],
[(int) $id],
$pid);
}
/**
* Claim, drain, and always release one worker's queue slice.
*
* @param int $pid
* @param int|string $max_records
* @param callable|null $heartbeat
*
* @return int
*/
function thold_notification_run($pid, $max_records = 'all', $heartbeat = null) {
if (is_callable($heartbeat)) {
$heartbeat();
}
$total_rows = 0;
try {
$total_rows = thold_notification_claim($pid);
thold_notification_execute($pid, $max_records, $heartbeat);
} finally {
try {
if (is_callable($heartbeat)) {
$heartbeat();
}
} finally {
thold_notification_release_claim($pid);
}
}
return $total_rows;
}
/**
* thold_notification_execute - drains one worker's claimed slice of the notification_queue:
* applies device-down notification-storm delay processing, then processes non-device
* notifications, then device notifications, with a heartbeat callback invoked between each phase.
*
* @param int $pid The worker process id that has already claimed queue rows to process.
* @param int|string $max_records The maximum number of records to process, or 'all'.
* @param callable|null $heartbeat Optional callback invoked between phases to refresh the
* worker's lease.
*
* @return void
*/
function thold_notification_execute($pid = 0, $max_records = 'all', $heartbeat = null) {
$pid = (int) $pid;
if ($pid <= 0) {
cacti_log('ERROR: Refusing to drain an unclaimed Thold notification queue.', false, 'THOLD');
return;
}
// See if and administrator has suspended notifications
$prev_suspended = read_config_option('thold_notification_suspended', true);
if (is_callable($heartbeat)) {
$heartbeat();
}
/**
* See if notification delay is active and mark the events as such,
* which will potentially leave less events to process.
* This process will also enable notification once the delay is over
* for the devices or allow them to be sent.
*/
pre_process_device_notifications();
if (is_callable($heartbeat)) {
$heartbeat();
}
/**
* Process any non-device up/down notifications first. These
* notifications are not subject to notification delay
*/
process_non_device_notifications($pid, $max_records, $prev_suspended, $heartbeat);
if (is_callable($heartbeat)) {
$heartbeat();
}
/**
* Last process expired notification delays or device
* notifications that are not subject to notification delay
* not matching any rule type.
*/
process_device_notifications($pid, $max_records, $prev_suspended, $heartbeat);
}
/**
* thold_notification_retry_delay - computes a bounded exponential backoff delay (in seconds) for
* a retried notification delivery attempt.
*
* @param int $attempt The attempt number (1-based; values below 1 are clamped to 1).
*
* @return int The delay in seconds, doubling per attempt and capped at 3600.
*/
function thold_notification_retry_delay($attempt) {
$attempt = max(1, (int) $attempt);
return min(3600, 60 * (2 ** ($attempt - 1)));
}
/**
* Values for the delivery-status columns, in the same order as their headers.
*
* @param array<string,mixed> $notification
*
* @return array<string,mixed>
*/
function thold_notification_queue_status_cells(array $notification) {
$processed = (int) ($notification['event_processed'] ?? 0);
return [
'event_processed' => $processed === 0 ? __('Pending', 'thold') : __('Done', 'thold'),
'error_code' => $processed === 0 ? __('N/A', 'thold') : ((int) ($notification['error_code'] ?? 0) > 0 ? __('Errored', 'thold') : __('Success', 'thold')),
'attempt_count' => (int) ($notification['attempt_count'] ?? 0),
'next_attempt' => !empty($notification['next_attempt']) ? $notification['next_attempt'] : __('N/A', 'thold'),
'event_processed_runtime' => $processed === 0 ? __('N/A', 'thold') : number_format_i18n($notification['event_processed_runtime'] ?? 0, 2),
];
}
/**
* Record one queued email delivery without losing transient failures.
*
* The fifth failed attempt is terminal. Earlier failures release the claim and
* schedule a bounded exponential retry, so a permanent SMTP error cannot spin
* every poller cycle forever.
*
* @param mixed $id
* @param int $pid
* @param mixed $error
* @param mixed $runtime
* @param mixed $previous_attempts
*/
function thold_notification_record_delivery($id, $pid, $error, $runtime, $previous_attempts = 0) {
return thold_notification_record_deliveries([(int) $id => $previous_attempts], $pid, $error, $runtime);
}
/**
* Record one grouped mail result with a single prepared update.
*
* @param array<int,int> $records Record ID => previous attempt count.
* @param int $pid Owning notification worker.
* @param string $error
* @param float $runtime
*
* @return bool
*/
function thold_notification_record_deliveries(array $records, $pid, $error, $runtime) {
$pid = (int) $pid;
if ($pid <= 0 || !cacti_sizeof($records)) {
return true;
}
$error = thold_notification_error_message($error);
$attempt_cases = [];
$attempt_params = [];
$next_cases = [];
$next_params = [];
$process_cases = [];
$process_params = [];
$done_cases = [];
$done_params = [];
$time_cases = [];
$time_params = [];
$ids = [];
foreach ($records as $id => $previous_attempts) {
$id = (int) $id;
if ($id <= 0) {
continue;
}
$attempt = max(0, (int) $previous_attempts) + 1;
$retryable = $error !== '' && $attempt < 5;
$done = $retryable ? 0 : 1;
$attempt_cases[] = 'WHEN ? THEN ?';
$attempt_params[] = $id;
$attempt_params[] = $attempt;
if ($retryable) {
$next_cases[] = 'WHEN ? THEN FROM_UNIXTIME(UNIX_TIMESTAMP() + ?)';
$next_params[] = $id;
$next_params[] = thold_notification_retry_delay($attempt);
$process_cases[] = 'WHEN ? THEN 0';
} else {
$next_cases[] = 'WHEN ? THEN NULL';
$next_params[] = $id;
$process_cases[] = 'WHEN ? THEN process_id';
}
$process_params[] = $id;
$done_cases[] = 'WHEN ? THEN ?';
$done_params[] = $id;
$done_params[] = $done;
$time_cases[] = $done ? 'WHEN ? THEN NOW()' : 'WHEN ? THEN event_processed_time';
$time_params[] = $id;
$ids[] = $id;
}
if (!cacti_sizeof($ids)) {
return true;
}
$placeholders = implode(',', array_fill(0, cacti_sizeof($ids), '?'));
$params = array_merge(
[$error === '' ? 0 : 1, $error],
$attempt_params,
$next_params,
$process_params,
$done_params,
$time_params,
[$runtime],
$ids,
[$pid]
);
return thold_notification_complete('UPDATE notification_queue
SET error_code = ?, error_message = ?,
attempt_count = CASE id ' . implode(' ', $attempt_cases) . ' ELSE attempt_count END,
next_attempt = CASE id ' . implode(' ', $next_cases) . ' ELSE next_attempt END,
process_id = CASE id ' . implode(' ', $process_cases) . ' ELSE process_id END,
event_processed = CASE id ' . implode(' ', $done_cases) . ' ELSE event_processed END,
event_processed_time = CASE id ' . implode(' ', $time_cases) . ' ELSE event_processed_time END,
event_processed_runtime = ?
WHERE id IN (' . $placeholders . ')
AND process_id = ?',
$params,
$ids,
$pid);
}
/**
* process_device_notifications - drains this worker's claimed device up/down notification_queue
* entries (mail and command topics), sending mail (optionally consolidating all device
* notifications for this run into a single digest email) or running the configured command, and
* recording each delivery result.
*
* @param int $pid The worker process id that has already claimed the queue rows to process.
* @param int|string $max_records The maximum number of records to process, or 'all'.
* @param bool $prev_suspended Whether notifications were already suspended before this run
* started (suppresses a duplicate suspend log line).
* @param callable|null $heartbeat Optional callback invoked between records to refresh the
* worker's lease.
*
* @return void
*/
function process_device_notifications($pid, $max_records, $prev_suspended, $heartbeat = null) {
$one_email = read_config_option('alert_deadnotify_one_mail') == 'on' ? true : false;
$emails = [];
fwrite(STDERR, 'THOLD_CI_DEBUG process_device_notifications: one_email=' . var_export($one_email, true) . ' raw=' . var_export(read_config_option('alert_deadnotify_one_mail'), true) . PHP_EOL);
if (!defined('TXT_SEP')) {
define('TXT_SEP', '----------------------------------------------------------');
}
if ($max_records == 'all') {
$sql_limit = '';
} else {
$sql_limit = 'LIMIT ' . (int) $max_records;
}
$records = db_fetch_assoc_prepared("SELECT *
FROM notification_queue
WHERE event_processed = 0
AND (next_attempt IS NULL OR next_attempt <= NOW())
AND topic IN ('thold_dhost_mail', 'thold_uhost_mail', 'thold_dhost_cmd', 'thold_uhost_cmd')
AND process_id = ?
ORDER BY event_time ASC
$sql_limit",
[$pid]);
fwrite(STDERR, 'THOLD_CI_DEBUG process_device_notifications: records=' . var_export($records, true) . ' prev_suspended=' . var_export($prev_suspended, true) . PHP_EOL);
if ($prev_suspended == 0) {
foreach ($records as $index => $r) {
if (is_callable($heartbeat)) {
$heartbeat();
}
$nstart = microtime(true);
// if notification is suspended, break from this loop
$suspended = read_config_option('thold_notification_suspended', true);
if ($suspended == 1) {
if ($prev_suspended == 0) {
debounce_run_notification('notify_suspend', 'WARNING: Notifications have been suspended by an operator. Returning from processing loop');
}
break;
}
$topic = $r['topic'];
$processed = false;
if ($one_email) {
$host_subject = read_config_option('alert_deadnotify_subject');
if ($host_subject == '') {
$host_subject = 'Thold Device Notification: Up/Down Devices Found';
}
}
switch($topic) {
case 'thold_dhost_mail':
case 'thold_uhost_mail':
$data = json_decode($r['event_data'], true);
$attributes = [
'from', 'to', 'cc', 'bcc', 'replyto', 'subject', 'body', 'body_text', 'attachments', 'headers', 'html'
];
foreach ($attributes as $a) {
if (isset($data[$a])) {
$$a = $data[$a];
} else {
$$a = '';
}
}
// reverse the base64 encoding
if (cacti_sizeof($attachments)) {
foreach ($attachments as $index => $attach) {
$attachments[$index]['attachment'] = base64_decode($attach['attachment'], true);
}
}
if (!$one_email) {
$error = mailer($from, $to, $cc, $bcc, $replyto, $subject, $body, $body_text, $attachments, $headers, $html);
if ($error != '') {
cacti_log("ERROR: Sending Email Failed To:$to Subject:$subject. Error was:'$error'", true, 'THOLD');
}
$nend = microtime(true);
thold_notification_record_delivery($r['id'], $pid, $error, $nend - $nstart, $r['attempt_count'] ?? 0);
} else {
$id = hash('sha256', json_encode([$from, $to, $cc, $bcc, $replyto]));
if (!isset($emails[$id])) {
$emails[$id]['from'] = $from;
$emails[$id]['to'] = $to;
$emails[$id]['cc'] = $cc;
$emails[$id]['bcc'] = $bcc;
$emails[$id]['replyto'] = $replyto;
$emails[$id]['subject'] = $host_subject;
$emails[$id]['body_text'] = $body_text;
$emails[$id]['body'] = str_replace(['<body>', '</body>'], '', $body);
$emails[$id]['pre_body'] = $subject;
$emails[$id]['pre_body_text'] = $subject;
$emails[$id]['html'] = $html;
$emails[$id]['headers'] = $headers;
if (cacti_sizeof($attachments)) {
$emails[$id]['attachments'] = $attachments;
} else {
$emails[$id]['attachments'] = [];
}
} else {
$emails[$id]['body_text'] .= PHP_EOL . TXT_SEP . PHP_EOL . $body_text;
$emails[$id]['body'] .= str_replace(['<body>', '</body>'], '', $body);
$emails[$id]['pre_body_text'] .= PHP_EOL . $subject;
$emails[$id]['pre_body'] .= '<br>' . $subject;
if (cacti_sizeof($attachments)) {
$emails[$id]['attachments'] += array_merge($emails[$id]['attachments'], $attachments);
}
}
$emails[$id]['records'][$r['id']] = $r['attempt_count'] ?? 0;
}
break;
case 'thold_dhost_cmd':
case 'thold_uhost_cmd':
$data = json_decode($r['event_data'], true);
$attributes = [
'environment', 'command', 'data'
];
foreach ($attributes as $a) {
if (isset($data[$a])) {
$$a = $data[$a];
} else {
$$a = '';
}
}
$output = [];
$return = 0;
if (cacti_sizeof($environment)) {
foreach ($environment as $e) {
putenv($e);
}
}
exec($command, $output, $return); // nosemgrep: php.lang.security.exec-use.exec-use -- admin-configured notification command; $command is built from thold_replace_threshold_tags + thold_expand_string with cacti_escapeshellarg protection
thold_process_command_output($output, $return, $topic, $data, $command);
$nend = microtime(true);
thold_notification_complete('UPDATE notification_queue
SET error_code = ?, error_message = ?, event_processed = 1, event_processed_time=NOW(), event_processed_runtime = ?
WHERE id = ?
AND process_id = ?',
[$return, thold_notification_error_message(implode("\n", $output)), $nend - $nstart, $r['id'], $pid],
[$r['id']],
$pid);
break;
default:
cacti_log(sprintf('ERROR: Unable to process Thold Notification of topic %s', $topic), false, 'THOLD');
thold_notification_reject_unknown_topic($r['id'] ?? 0, $pid, $topic);
}
}
if (cacti_sizeof($emails) && $one_email) {
foreach ($emails as $email) {
if (is_callable($heartbeat)) {
$heartbeat();
}
$attachments = [];
$from = $email['from'];
$to = $email['to'];
$cc = $email['cc'];
$bcc = $email['bcc'];
$replyto = $email['replyto'];
$subject = $email['subject'];
$body = $email['body'];
$body_text = $email['body_text'];
$headers = $email['headers'];
$html = $email['html'];
$pre_body = $email['pre_body'];
$pre_body_text = $email['pre_body_text'];
if (cacti_sizeof($email['attachments'])) {
foreach ($email['attachments'] as $index => $attach) {
$attachments[$index]['attachment'] = base64_decode($attach['attachment'], true);
}
}
$body = $pre_body . '<br><hr><br>' . $body;
$body_text = $pre_body_text . PHP_EOL . PHP_EOL . $body_text;
cacti_log("NOTE: Sending One Email Device Notification to:$to, bcc:$bcc, from:{$from[0]}, subject: $subject", false, 'THOLD');
$error = mailer($from, $to, $cc, $bcc, $replyto, $subject, $body, $body_text, $attachments, $headers, $html);
if ($error != '') {
cacti_log("ERROR: Sending Email Failed To:$to Subject:$subject. Error was:'$error'", true, 'THOLD');
}
$nend = microtime(true);
thold_notification_record_deliveries($email['records'], $pid, $error, $nend - $nstart);
}
}
} else {
debounce_run_notification('notify_suspend', 'WARNING: Notifications have been suspended by an operator. Returning from processing loop');
}
}
/**
* process_non_device_notifications - drains this worker's claimed threshold notification_queue
* entries (mail and command topics, excluding device up/down topics), sending mail or running the
* configured command, and recording each delivery result.
*
* @param int $pid The worker process id that has already claimed the queue rows to process.
* @param int|string $max_records The maximum number of records to process, or 'all'.
* @param bool $prev_suspended Whether notifications were already suspended before this run
* started (suppresses a duplicate suspend log line).
* @param callable|null $heartbeat Optional callback invoked between records to refresh the
* worker's lease.
*
* @return void
*/
function process_non_device_notifications($pid, $max_records, $prev_suspended, $heartbeat = null) {
if ($max_records == 'all') {
$sql_limit = '';
} else {
$sql_limit = 'LIMIT ' . (int) $max_records;
}
$records = db_fetch_assoc_prepared("SELECT *
FROM notification_queue
WHERE event_processed = 0
AND (next_attempt IS NULL OR next_attempt <= NOW())
AND topic NOT IN ('thold_dhost_mail', 'thold_uhost_mail', 'thold_dhost_cmd', 'thold_uhost_cmd')
AND process_id = ?
ORDER BY event_time ASC
$sql_limit",
[$pid]);
if ($prev_suspended == 0) {
foreach ($records as $r) {
if (is_callable($heartbeat)) {
$heartbeat();
}
$nstart = microtime(true);
// if notification is suspended, break from this loop
$suspended = read_config_option('thold_notification_suspended', true);
if ($suspended == 1) {
if ($prev_suspended == 0) {
debounce_run_notification('notify_suspend', 'WARNING: Notifications have been suspended by an operator. Returning from processing loop');
}
break;
}
$topic = $r['topic'];
$processed = false;
switch($topic) {
case 'thold_mail':
$data = json_decode($r['event_data'], true);
$attributes = [
'from', 'to', 'cc', 'bcc', 'replyto', 'subject', 'body', 'body_text', 'attachments', 'headers', 'html'
];
foreach ($attributes as $a) {
if (isset($data[$a])) {
$$a = $data[$a];
} else {
$$a = '';
}
}
if (cacti_sizeof($attachments)) {
foreach ($attachments as $index => $attach) {
$attachments[$index]['attachment'] = base64_decode($attach['attachment'], true);
}
}
$error = mailer($from, $to, $cc, $bcc, $replyto, $subject, $body, $body_text, $attachments, $headers, $html);
if ($error != '') {
cacti_log("ERROR: Sending Email Failed To:$to Subject:$subject. Error was:'$error'", true, 'THOLD');
}
$nend = microtime(true);
thold_notification_record_delivery($r['id'], $pid, $error, $nend - $nstart, $r['attempt_count'] ?? 0);
break;
case 'thold_cmd':
$data = json_decode($r['event_data'], true);
$attributes = [
'environment', 'command', 'data'
];
foreach ($attributes as $a) {
if (isset($data[$a])) {
$$a = $data[$a];
} else {
$$a = '';
}
}
$output = [];
$return = 0;
if (cacti_sizeof($environment)) {
foreach ($environment as $e) {
putenv($e);
}
}
exec($command, $output, $return); // nosemgrep: php.lang.security.exec-use.exec-use -- admin-configured notification command; $command is built from thold_replace_threshold_tags + thold_expand_string with cacti_escapeshellarg protection
thold_process_command_output($output, $return, $topic, $data, $command);
$nend = microtime(true);
thold_notification_complete('UPDATE notification_queue
SET error_code = ?, error_message = ?, event_processed = 1, event_processed_time=NOW(), event_processed_runtime = ?
WHERE id = ?
AND process_id = ?',
[$return, thold_notification_error_message(implode("\n", $output)), $nend - $nstart, $r['id'], $pid],
[$r['id']],
$pid);
break;
default:
cacti_log(sprintf('ERROR: Unable to process Thold Notification of topic %s', $topic), false, 'THOLD');
thold_notification_reject_unknown_topic($r['id'] ?? 0, $pid, $topic);
}
}
} else {
debounce_run_notification('notify_suspend', 'WARNING: Notifications have been suspended by an operator. Returning from processing loop');
}
}
/**
* thold_template_update_threshold - re-applies a threshold template's settings, contact
* assignments, notification list, and suggested name to a single templated threshold.
*
* @param int $id The threshold id to update.
* @param int $template The threshold template id to copy settings from.
*
* @return void
*/
function thold_template_update_threshold($id, $template) {
db_execute_prepared("UPDATE thold_data AS td, thold_template AS tt
SET
td.name = tt.suggested_name, td.thold_hi = tt.thold_hi,
td.data_source_name = tt.data_source_name, td.data_template_hash = tt.data_template_hash,
td.data_template_id = tt.data_template_id, td.thold_low = tt.thold_low,
td.thold_fail_trigger = tt.thold_fail_trigger, td.time_hi = tt.time_hi, td.time_low = tt.time_low,
td.time_fail_trigger = tt.time_fail_trigger, td.time_fail_length = tt.time_fail_length,
td.thold_warning_hi = tt.thold_warning_hi, td.thold_warning_low = tt.thold_warning_low,
td.thold_warning_fail_trigger = tt.thold_warning_fail_trigger, td.time_warning_hi = tt.time_warning_hi,
td.time_warning_low = tt.time_warning_low, td.time_warning_fail_trigger = tt.time_warning_fail_trigger,
td.time_warning_fail_length = tt.time_warning_fail_length, td.thold_enabled = tt.thold_enabled,
td.thold_type = tt.thold_type, td.bl_type = tt.bl_type, td.bl_cf = tt.bl_cf, td.bl_ref_time_range = tt.bl_ref_time_range,
td.bl_pct_up = tt.bl_pct_up, td.bl_pct_down = tt.bl_pct_down, td.bl_pct_up = tt.bl_pct_up,
td.bl_fail_trigger = tt.bl_fail_trigger, td.bl_alert = tt.bl_alert, td.bl_thold_valid = 0,
td.repeat_alert = tt.repeat_alert, td.data_type = tt.data_type, td.cdef = tt.cdef,
td.percent_ds = tt.percent_ds, td.expression = tt.expression, td.upper_ds = tt.upper_ds,
td.exempt = tt.exempt, td.reset_ack = tt.reset_ack, td.persist_ack = tt.persist_ack,
td.thold_hrule_alert = tt.thold_hrule_alert, td.thold_hrule_warning = tt.thold_hrule_warning,
td.skipscale = tt.skipscale, td.restored_alert = tt.restored_alert,
td.email_subject = tt.email_subject, td.email_subject_warn = tt.email_subject_warn,
td.email_subject_restoral = tt.email_subject_restoral, td.email_body = tt.email_body,
td.email_body_warn = tt.email_body_warn, td.email_body_restoral = tt.email_body_restoral,
td.trigger_cmd_high = tt.trigger_cmd_high, td.trigger_cmd_low = tt.trigger_cmd_low,
td.trigger_cmd_norm = tt.trigger_cmd_norm, td.syslog_enabled = tt.syslog_enabled,
td.syslog_priority = tt.syslog_priority, td.syslog_facility = tt.syslog_facility,
td.snmp_event_category = tt.snmp_event_category, td.snmp_event_description = tt.snmp_event_description,
td.snmp_event_severity = tt.snmp_event_severity, td.snmp_event_warning_severity = tt.snmp_event_warning_severity,
td.notes = tt.notes, td.external_id = tt.external_id, td.format_file = tt.format_file,
td.graph_timespan = tt.graph_timespan
WHERE td.id = ?
AND td.template_enabled = 'on'
AND tt.id = ?",
[$id, $template]);
db_execute_prepared('DELETE FROM plugin_thold_threshold_contact
WHERE thold_id = ?',
[$id]);
db_execute_prepared('INSERT INTO plugin_thold_threshold_contact
(thold_id, contact_id)
SELECT ?, contact_id
FROM plugin_thold_template_contact
WHERE template_id = ?',
[$id, $template]);
update_notification_list_from_template($template, $id);
update_suggested_names_from_template($template, $id);
}
/**
* thold_template_update_thresholds - re-applies a threshold template's settings and contact
* assignments to every enabled templated threshold, then refreshes their notification lists and
* suggested names.
*
* @param int $id The threshold template id whose thresholds should be resynced.
*
* @return void
*/
function thold_template_update_thresholds($id) {
db_execute_prepared("UPDATE thold_data AS td, thold_template AS tt
SET
td.name = tt.suggested_name, td.thold_hi = tt.thold_hi, td.data_source_name = tt.data_source_name,
td.data_template_hash = tt.data_template_hash, td.data_template_id = tt.data_template_id,
td.thold_low = tt.thold_low, td.thold_fail_trigger = tt.thold_fail_trigger, td.time_hi = tt.time_hi,
td.time_low = tt.time_low, td.time_fail_trigger = tt.time_fail_trigger,
td.time_fail_length = tt.time_fail_length, td.thold_warning_hi = tt.thold_warning_hi,
td.thold_warning_low = tt.thold_warning_low, td.thold_warning_fail_trigger = tt.thold_warning_fail_trigger,
td.time_warning_hi = tt.time_warning_hi, td.time_warning_low = tt.time_warning_low,
td.time_warning_fail_trigger = tt.time_warning_fail_trigger, td.time_warning_fail_length = tt.time_warning_fail_length,
td.thold_enabled = tt.thold_enabled, td.thold_type = tt.thold_type,
td.bl_type = tt.bl_type, td.bl_cf = tt.bl_cf, td.bl_ref_time_range = tt.bl_ref_time_range,
td.bl_pct_up = tt.bl_pct_up, td.bl_pct_down = tt.bl_pct_down, td.bl_pct_up = tt.bl_pct_up,
td.bl_fail_trigger = tt.bl_fail_trigger, td.bl_alert = tt.bl_alert, td.bl_thold_valid = 0,
td.repeat_alert = tt.repeat_alert, td.data_type = tt.data_type, td.cdef = tt.cdef,
td.percent_ds = tt.percent_ds, td.expression = tt.expression, td.upper_ds = tt.upper_ds, td.show_units = tt.show_units,
td.units_suffix = tt.units_suffix, td.decimals = tt.decimals, td.exempt = tt.exempt,
td.reset_ack = tt.reset_ack, td.persist_ack = tt.persist_ack,
td.thold_hrule_alert = tt.thold_hrule_alert, td.thold_hrule_warning = tt.thold_hrule_warning,
td.skipscale = tt.skipscale, td.restored_alert = tt.restored_alert,
td.email_subject = tt.email_subject, td.email_subject_warn = tt.email_subject_warn,
td.email_subject_restoral = tt.email_subject_restoral, td.email_body = tt.email_body,
td.email_body_warn = tt.email_body_warn, td.email_body_restoral = tt.email_body_restoral,
td.trigger_cmd_high = tt.trigger_cmd_high, td.trigger_cmd_low = tt.trigger_cmd_low, td.trigger_cmd_norm = tt.trigger_cmd_norm,
td.syslog_enabled = tt.syslog_enabled, td.syslog_priority = tt.syslog_priority,
td.syslog_facility = tt.syslog_facility,
td.snmp_event_category = tt.snmp_event_category, td.snmp_event_description = tt.snmp_event_description,
td.snmp_event_severity = tt.snmp_event_severity, td.snmp_event_warning_severity = tt.snmp_event_warning_severity,
td.notes = tt.notes, td.external_id = tt.external_id, td.format_file = tt.format_file,
td.graph_timespan = tt.graph_timespan
WHERE td.thold_template_id = ?
AND td.template_enabled = 'on'
AND tt.id = ?",
[$id, $id]);
$rows = db_fetch_assoc_prepared("SELECT id, thold_template_id
FROM thold_data
WHERE thold_data.thold_template_id = ?
AND thold_data.template_enabled = 'on'",
[$id]);
if (cacti_sizeof($rows)) {
foreach ($rows as $row) {
db_execute_prepared('DELETE FROM plugin_thold_threshold_contact
WHERE thold_id = ?',
[$row['id']]);
db_execute_prepared('INSERT INTO plugin_thold_threshold_contact
(thold_id, contact_id)
SELECT ?, contact_id
FROM plugin_thold_template_contact
WHERE template_id = ?',
[$row['id'], $row['thold_template_id']]);
}
}
update_notification_list_from_template($id);
update_suggested_names_from_template($id);
}
/**
* update_notification_list_from_template - copies a threshold template's notification list
* settings onto its templated threshold(s), when the template is configured to enforce them.
*
* @param int $id The threshold template id to copy notification settings from.
* @param int $thold_id Optional single threshold id to restrict the update to; -1 updates all
* thresholds using the template.
*
* @return void
*/
function update_notification_list_from_template($id, $thold_id = -1) {
$templated = db_fetch_cell_prepared('SELECT notify_templated
FROM thold_template
WHERE id = ?',
[$id]);
if ($thold_id > 0) {
$sql_where = ' AND td.id = ' . $thold_id;
} else {
$sql_where = '';
}
if ($templated == 'on') {
db_execute_prepared("UPDATE thold_data AS td, thold_template AS tt
SET
td.notify_warning = tt.notify_warning, td.notify_alert = tt.notify_alert,
td.notify_extra = tt.notify_extra, td.notify_warning_extra = tt.notify_warning_extra
WHERE td.thold_template_id = ?
AND td.template_enabled = 'on'
AND tt.id = ?" . $sql_where,
[$id, $id]);
}
}
/**
* update_suggested_names_from_template - recomputes and persists the cached display name
* (name_cache) for a threshold template's threshold(s) from its suggested_name pattern.
*
* @param int $id The threshold template id providing the suggested_name pattern.
* @param int $thold_id Optional single threshold id to restrict the update to; -1 updates all
* thresholds using the template.
*
* @return void
*/
function update_suggested_names_from_template($id, $thold_id = -1) {
$suggested_name = db_fetch_cell_prepared('SELECT suggested_name
FROM thold_template
WHERE id = ?',
[$id]);
if ($thold_id > 0) {
$sql_where = ' AND id = ' . $thold_id;
} else {
$sql_where = '';
}
$tholds = db_fetch_assoc_prepared('SELECT *
FROM thold_data
WHERE template_enabled = "on"
AND thold_template_id = ?' . $sql_where,
[$id]);
if (cacti_sizeof($tholds)) {
foreach ($tholds as $thold_data) {
$name_cache = thold_expand_string($thold_data, $suggested_name);
db_execute_prepared('UPDATE thold_data
SET name_cache = ?
WHERE id = ?',
[$name_cache, $thold_data['id']]);
}
}
}
/**
* thold_cacti_log - writes a thold-prefixed message to the Cacti log file and/or syslog per the
* 'log_destination' setting, optionally also echoing a running-elapsed-time-prefixed line to
* stdout.
*
* @param string $string The message to log.
* @param string $thread Optional thread/context identifier included in the log prefix.
* @param bool $stdout Whether to also print the message (with elapsed time) to stdout.
*
* @global array $config Cacti's global configuration array.
* @global bool $debug Whether debug logging is enabled for this request.
*
* @return void
*/
function thold_cacti_log($string, $thread = '', $stdout = false) {
global $config, $debug;
static $start = null;
$environ = 'THOLD' . ($thread != '' ? ' THREAD[' . $thread . ']' : '');
// fill in the current date for printing in the log
if (defined('CACTI_DATE_TIME_FORMAT')) {
$date = date(CACTI_DATE_TIME_FORMAT);
} else {
$date = date('Y-m-d H:i:s');
}
// determine how to log data
$logdestination = read_config_option('log_destination');
$logfile = read_config_option('path_cactilog');
// format the message
if ($stdout) {
if ($start == null) {
$start = microtime(true);
$total = 0.00;
} else {
$total = microtime(true) - $start;
}
if (!$debug) {
printf('Total[%0.4f] - %s: %s' . PHP_EOL, $total, $environ, trim($string));
} else {
printf('Total[%0.4f] DEBUG: %s' . PHP_EOL, $total, trim($string));
}
}
$message = "$date - " . $environ . ': ' . trim($string) . "\n";
// Log to Logfile
if ((($logdestination == 1) || ($logdestination == 2)) && (read_config_option('log_verbosity') != POLLER_VERBOSITY_NONE)) {
if ($logfile == '') {
$logfile = $config['base_path'] . '/log/cacti.log';
}
// print the data to the log (append)
$fp = @fopen($logfile, 'a');
if ($fp) {
@fwrite($fp, $message);
fclose($fp);
}
}
// Log to Syslog/Eventlog
// Syslog is currently Unstable in Win32
if (($logdestination == 2) || ($logdestination == 3)) {
$string = strip_tags($string);
$log_type = '';
if (substr_count($string,'ERROR:')) {
$log_type = 'err';
} elseif (substr_count($string,'WARNING:')) {
$log_type = 'warn';
} elseif (substr_count($string,'STATS:')) {
$log_type = 'stat';
} elseif (substr_count($string,'NOTICE:')) {
$log_type = 'note';
}
if (strlen($log_type)) {
if ($config['cacti_server_os'] == 'win32') {
openlog('Cacti', LOG_NDELAY | LOG_PID, LOG_USER);
} else {
openlog('Cacti', LOG_NDELAY | LOG_PID, LOG_SYSLOG);
}
if (($log_type == 'err') && (read_config_option('log_perror'))) {
syslog(LOG_CRIT, $environ . ': ' . $string);
}
if (($log_type == 'warn') && (read_config_option('log_pwarn'))) {
syslog(LOG_WARNING, $environ . ': ' . $string);
}
if ((($log_type == 'stat') || ($log_type == 'note')) && (read_config_option('log_pstats'))) {
syslog(LOG_INFO, $environ . ': ' . $string);
}
closelog();
}
}
}
/**
* thold_threshold_enable - enables a threshold (per-threshold enable flag) and resets its fail
* counters/alert state, subject to the caller's realm authorization.
*
* @param int $id The threshold id to enable.
*
* @return void
*/
function thold_threshold_enable($id) {
if (api_user_realm_auth('thold.php')) {
db_execute_prepared("UPDATE thold_data
SET thold_per_enabled = 'on',
thold_fail_count = 0,
thold_warning_fail_count = 0,
bl_fail_count = 0,
thold_alert = 0,
bl_alert = 0
WHERE id = ?",
[$id]);
}
}
/**
* thold_threshold_disable - disables a threshold (per-threshold enable flag) and resets its fail
* counters/alert state, subject to the caller's realm authorization.
*
* @param int $id The threshold id to disable.
*
* @return void
*/
function thold_threshold_disable($id) {
if (api_user_realm_auth('thold.php')) {
db_execute_prepared("UPDATE thold_data
SET thold_per_enabled = '',
thold_fail_count = 0,
thold_warning_fail_count = 0,
bl_fail_count = 0,
thold_alert = 0,
bl_alert = 0
WHERE id = ?",
[$id]);
}
}
/**
* thold_threshold_ack_prompt - renders the confirmation form (with an optional operator comment
* field) shown before acknowledging a threshold.
*
* @param int $id The threshold id being acknowledged.
*
* @global array $config Cacti's global configuration array.
*
* @return void
*/
function thold_threshold_ack_prompt($id) {
global $config;
top_header();
form_start($config['url_path'] . 'plugins/thold/thold_graph.php');
html_start_box(__('Acknowledge Threshold', 'thold'), '60%', '', '3', 'center', '');
$message = __('Click \'Continue\' to Acknowledge the following Threshold(s).', 'thold');
$button = __esc('Acknowledge Threshold(s)', 'thold');
print "<tr>
<td colspan='2'>
<p>$message</p>
</td>
</tr>";
print "<tr><td colspan='2'><p><i>Operator Message:</i><br><textarea class='ui-state-default ui-corner-all' style='width:70%;height:50px;' area-multiline='true' rows='2' id='message' name='message'></textarea></p></td></tr>";
$save_html = "<input type='button' class='ui-button ui-corner-all ui-widget' value='" . __esc('Cancel', 'thold') . "' onClick='cactiReturnTo()'>";
if (!empty($button)) {
$save_html .= "&nbsp;<input type='submit' class='ui-button ui-corner-all ui-widget' value='" . __esc('Continue', 'thold') . "' title='$button'>";
}
print "<tr>
<td colspan='2' class='saveRow'>
<input type='hidden' name='action' value='ack_confirm'>
<input type='hidden' name='threshold_id' value='$id'>
$save_html
</td>
</tr>";
html_end_box();
form_end();
bottom_footer();
}
/**
* thold_threshold_ack - acknowledges a threshold (clearing its acknowledgment flag) and records
* the acknowledgment, subject to the caller's realm authorization and graph permissions.
*
* @param int $id The threshold id to acknowledge.
*
* @return void
*/
function thold_threshold_ack($id) {
if (api_user_realm_auth('thold.php')) {
if (is_thold_allowed($id)) {
plugin_thold_log_changes($id, 'acknowledge_threshold', ['id' => $id]);
db_execute_prepared('UPDATE thold_data
SET acknowledgment = ""
WHERE id = ?',
[$id]);
ack_logging($id, get_request_var('message'));
}
}
}
/**
* thold_threshold_suspend_ack - clears a threshold's acknowledgment flag (only when the threshold
* is configured to auto-reset acknowledgment), subject to the caller's realm authorization and
* graph permissions.
*
* @param int $id The threshold id to update.
*
* @return void
*/
function thold_threshold_suspend_ack($id) {
if (api_user_realm_auth('thold.php')) {
if (is_thold_allowed($id)) {
plugin_thold_log_changes($id, 'suspend_acknowledge_threshold', ['id' => $id]);
db_execute_prepared('UPDATE thold_data
SET acknowledgment=""
WHERE id = ?
AND reset_ack = "on"',
[$id]);
}
}
}
/**
* thold_threshold_resume_ack - re-applies acknowledgment to a still-alerting threshold configured
* to auto-reset acknowledgment, subject to the caller's realm authorization and graph permissions.
*
* @param int $id The threshold id to update.
*
* @return void
*/
function thold_threshold_resume_ack($id) {
if (api_user_realm_auth('thold.php')) {
if (is_thold_allowed($id)) {
plugin_thold_log_changes($id, 'resume_acknowledge_threshold', ['id' => $id]);
db_execute_prepared('UPDATE thold_data
SET acknowledgment="on"
WHERE id = ?
AND thold_alert > 0
AND reset_ack = "on"',
[$id]);
}
}
}
/**
* get_thold_emails - resolves the recipient (to/bcc) address list for a threshold's alert or
* warning notification, combining its legacy per-threshold contacts/extra addresses with its
* assigned notification list's addresses.
*
* @param array $thold The threshold row, including notify_alert/notify_warning list ids and
* notify_extra/notify_warning_extra legacy addresses.
* @param string $class Which notification phase to resolve addresses for: 'alert' or 'warning'.
* @param string $recipient Which address type to resolve: 'to' or 'bcc'.
*
* @return string The comma-separated resolved address list.
*/
function get_thold_emails($thold, $class = 'alert', $recipient = 'to') {
$emails = '';
if ($class == 'alert') {
if (read_config_option('thold_disable_legacy') != 'on' && $recipient == 'to') {
$email_addresses = [];
$rows = db_fetch_assoc_prepared('SELECT ptc.data
FROM plugin_thold_contacts AS ptc
INNER JOIN plugin_thold_threshold_contact AS pttc
ON ptc.id = pttc.contact_id
WHERE pttc.thold_id = ?',
[$thold['id']]);
if (cacti_sizeof($rows)) {
foreach ($rows as $row) {
$email_addresses[] = $row['data'];
}
}
$emails = implode(',', $email_addresses);
if ($emails != '') {
$emails .= ',' . $thold['notify_extra'];
} else {
$emails = $thold['notify_extra'];
}
}
$emails .= ($emails != '' ? ',' : '') . get_thold_notification_emails($thold['notify_alert'], $recipient);
} else {
if (read_config_option('thold_disable_legacy') != 'on' && $recipient == 'to') {
$emails = $thold['notify_warning_extra'];
}
$emails .= ($emails != '' ? ',' : '') . get_thold_notification_emails($thold['notify_warning'], $recipient);
}
return $emails;
}
/**
* get_thold_notification_emails - resolves a notification list's to/bcc address list, when the
* list is enabled.
*
* @param int|string $id The notification list id to look up.
* @param string $recipient Which address column to resolve: 'to' or 'bcc'.
*
* @return string The resolved address list, or an empty string when unset or the list is disabled.
*/
function get_thold_notification_emails($id = '', $recipient = 'to') {
if ($id == '' || $id === null) {
return '';
}
if (!thold_notification_list_enabled($id)) {
return '';
}
if (!empty($id)) {
if ($recipient == 'to') {
return trim(db_fetch_cell_prepared('SELECT emails
FROM plugin_notification_lists
WHERE id = ?',
[$id]));
} else {
return trim(db_fetch_cell_prepared('SELECT bcc_emails
FROM plugin_notification_lists
WHERE id = ?',
[$id]));
}
} else {
return '';
}
}
/**
* thold_notification_list_enabled - checks whether a notification list is enabled, caching every
* list's enabled state for the rest of the request.
*
* @param int $list_id The notification list id to check.
*
* @return bool True when the list is enabled, or when the list id isn't found (fails open).
*/
function thold_notification_list_enabled($list_id) {
static $lists = null;
if ($lists == null) {
$lists = array_rekey(
db_fetch_assoc('SELECT id, enabled
FROM plugin_notification_lists'),
'id', 'enabled'
);
}
if (isset($lists[$list_id])) {
return $lists[$list_id] == 'on';
} else {
return true;
}
}
/** get_hash_thold_template - returns the current unique hash for a thold_template
* @param $id - (int) the ID of the thold template to return a hash for
* @param mixed $id
* @returns - a 128-bit, hexadecimal hash */
function get_hash_thold_template($id) {
$hash = db_fetch_cell_prepared('SELECT hash
FROM thold_template
WHERE id = ?',
[$id]);
if (preg_match('/[a-fA-F0-9]{32}/', $hash)) {
return $hash;
} else {
return generate_hash();
}
}
/**
* ia2xml - recursively renders an associative array as XML elements, HTML-escaping scalar values.
*
* @param array $array The array to render, with keys used as element names.
*
* @return string The rendered XML fragment (without a root wrapper element).
*/
function ia2xml($array) {
$xml = '';
if (cacti_sizeof($array)) {
foreach ($array as $key=>$value) {
if (is_array($value)) {
$xml .= "\t<$key>" . ia2xml($value) . "</$key>\n";
} else {
$xml .= "\t<$key>" . html_escape($value) . "</$key>\n";
}
}
}
return $xml;
}
/**
* array2xml - wraps an array's rendered XML (via ia2xml()) in a uniquely-numbered root element,
* used when exporting multiple threshold templates in one XML document.
*
* @param array $array The array to render.
* @param string $tag The root element name prefix; suffixed with an incrementing index.
*
* @return string The rendered XML document fragment.
*/
function array2xml($array, $tag = 'template') {
static $index = 1;
$xml = "<$tag$index>\n" . ia2xml($array) . "</$tag$index>\n";
$index++;
return $xml;
}
/**
* thold_snmptrap - sends a CACTI-THOLD-MIB SNMP notification for a threshold event, when the
* Cacti SNMPAgent plugin is active.
*
* @param array $varbinds The event varbind data (see get_thold_snmp_data()), with `<HOSTIP>`
* resolved into the event description when present.
* @param int $severity The SNMPAgent event severity to send at.
* @param bool $overwrite Whether to overwrite any pending notification of the same type.
*
* @return void
*/
function thold_snmptrap($varbinds, $severity = SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite = false) {
if (function_exists('snmpagent_notification')) {
if (isset($varbinds['eventDescription']) && isset($varbinds['eventDeviceIp'])) {
$varbinds['eventDescription'] = thold_str_replace('<HOSTIP>', $varbinds['eventDeviceIp'], $varbinds['eventDescription']);
}
snmpagent_notification('tholdNotify', 'CACTI-THOLD-MIB', $varbinds, $severity, $overwrite);
} else {
cacti_log('ERROR: THOLD was unable to generate SNMP notifications. Cacti SNMPAgent plugin is current missing or inactive.');
}
}
/**
* thold_prune_old_data - removes thresholds, logs, and failed-host records left orphaned by
* deleted devices/graphs, and purges old processed notification_queue entries.
*
* @return void
*/
function thold_prune_old_data() {
// Remove failed entries from removed devices
db_execute('DELETE pthf
FROM plugin_thold_host_failed AS pthf
LEFT JOIN host AS h
ON pthf.host_id = h.id
WHERE h.id IS NULL');
if (db_affected_rows() > 0) {
set_config_option('time_last_change_thold_device', time());
}
$queue = read_config_option('thold_notification_queue');
// Remove log entries from removed devices
db_execute('DELETE ptl
FROM plugin_thold_log AS ptl
LEFT JOIN host AS h
ON ptl.host_id = h.id
WHERE h.id IS NULL');
if (db_affected_rows() > 0) {
set_config_option('time_last_change_thold_log', time());
set_config_option('time_last_change_thold', time());
}
// Remove thresholds from removed devices
db_execute('DELETE td
FROM thold_data AS td
LEFT JOIN host AS h
ON td.host_id = h.id
WHERE h.id IS NULL');
if (db_affected_rows() > 0) {
set_config_option('time_last_change_thold', time());
}
// Remove thresholds from removed graphs
db_execute('DELETE td
FROM thold_data AS td
LEFT JOIN graph_local AS gl
ON td.local_graph_id = gl.id
WHERE gl.id IS NULL');
if (db_affected_rows() > 0) {
set_config_option('time_last_change_thold', time());
}
if ($queue == 'on') {
db_execute('DELETE FROM notification_queue
WHERE event_processed = 1
AND event_processed_time < FROM_UNIXTIME(UNIX_TIMESTAMP()-3600)');
if (db_affected_rows() > 0) {
db_execute('OPTIMIZE TABLE notification_queue');
}
}
}
/**
* thold_get_allowed_devices - fetches the devices a user is permitted to see (via their graph
* permissions), applying graph authentication policy filtering and optional caller-supplied
* WHERE/ORDER BY/LIMIT clauses.
*
* @param string $sql_where Additional SQL WHERE conditions (without the WHERE keyword).
* @param string $order_by SQL ORDER BY expression (without the ORDER BY keyword).
* @param string $sql_limit SQL LIMIT clause contents (without the LIMIT keyword).
* @param int &$total_rows Set to the total matching row count (ignoring $sql_limit).
* @param int $user_id The user id to check permissions for; 0 uses the session user, -1 skips
* authentication entirely.
* @param int $device_id Optional device id to restrict results to a single device.
*
* @return array The matching host rows, including computed graph/data-source counts and instate.
*/
function thold_get_allowed_devices($sql_where = '', $order_by = 'description', $sql_limit = '', &$total_rows = 0, $user_id = 0, $device_id = 0) {
if ($user_id == -1) {
$auth_method = 0;
} else {
$auth_method = read_config_option('auth_method');
}
if ($user_id == 0) {
if (isset($_SESSION['sess_user_id'])) {
$user_id = $_SESSION['sess_user_id'];
} else {
return [];
}
}
$simple_perms = get_simple_device_perms($user_id);
$init_rows = $total_rows;
$host_list = [];
if ($sql_limit != '' && $sql_limit != -1) {
$sql_limit = "LIMIT $sql_limit";
} else {
$sql_limit = '';
}
if ($order_by != '') {
$order_by = "ORDER BY $order_by";
}
if (read_user_setting('hide_disabled') == 'on') {
$sql_where .= ($sql_where != '' ? ' AND' : '') . ' (h.disabled = "" OR h.disabled IS NULL)';
}
if ($sql_where != '') {
$sql_where = "WHERE $sql_where";
}
if ($device_id > 0) {
$sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . " h.id = $device_id";
}
$graph_auth_method = read_config_option('graph_auth_method');
$poller_interval = read_config_option('poller_interval');
// get policies for all groups and user
$policies = get_policies($user_id);
if (!$simple_perms && $auth_method != 0) {
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
}
if (db_column_exists('host', 'graphs')) {
$host_list = db_fetch_assoc("SELECT h1.*, graphs, data_sources,
CAST(IF(availability_method = 0, '0',
IF(status_event_count > 0 AND status IN (1, 2), status_event_count*$poller_interval,
IF(UNIX_TIMESTAMP(status_rec_date) < 943916400 AND status IN (0, 3), total_polls*$poller_interval,
IF(UNIX_TIMESTAMP(status_rec_date) > 943916400, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(status_rec_date),
IF(snmp_sysUptimeInstance>0 AND snmp_version > 0, snmp_sysUptimeInstance/100, UNIX_TIMESTAMP()
))))) AS unsigned) AS instate
FROM host AS h1
INNER JOIN (
SELECT DISTINCT id
FROM (
SELECT h.id
FROM host AS h
LEFT JOIN graph_local AS gl
ON h.id = gl.host_id
LEFT JOIN graph_templates AS gt
ON gt.id = gl.graph_template_id
LEFT JOIN host_template AS ht
ON h.host_template_id = ht.id
$sql_where
) AS rs1
) AS rs2
ON rs2.id = h1.id
$order_by
$sql_limit"
);
$sql = "SELECT COUNT(DISTINCT id)
FROM (
SELECT h.id
FROM host AS h
LEFT JOIN graph_local AS gl
ON h.id = gl.host_id
LEFT JOIN graph_templates AS gt
ON gt.id = gl.graph_template_id
LEFT JOIN host_template AS ht
ON h.host_template_id = ht.id
$sql_where
) AS rower";
} else {
$host_list = db_fetch_assoc("SELECT h1.*, graphs, data_sources,
CAST(IF(availability_method = 0, '0',
IF(status_event_count > 0 AND status IN (1, 2), status_event_count*$poller_interval,
IF(UNIX_TIMESTAMP(status_rec_date) < 943916400 AND status IN (0, 3), total_polls*$poller_interval,
IF(UNIX_TIMESTAMP(status_rec_date) > 943916400, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(status_rec_date),
IF(snmp_sysUptimeInstance>0 AND snmp_version > 0, snmp_sysUptimeInstance/100, UNIX_TIMESTAMP()
))))) AS unsigned) AS instate
FROM host AS h1
INNER JOIN (
SELECT DISTINCT id
FROM (
SELECT h.id
FROM host AS h
LEFT JOIN graph_local AS gl
ON h.id = gl.host_id
LEFT JOIN graph_templates AS gt
ON gt.id = gl.graph_template_id
LEFT JOIN host_template AS ht
ON h.host_template_id = ht.id
$sql_where
) AS rs1
) AS rs2
ON rs2.id = h1.id
LEFT JOIN (SELECT host_id, COUNT(*) AS graphs FROM graph_local GROUP BY host_id) AS gl
ON h1.id = gl.host_id
LEFT JOIN (SELECT host_id, COUNT(*) AS data_sources FROM data_local GROUP BY host_id) AS dl
ON h1.id = dl.host_id
$order_by
$sql_limit"
);
$sql = "SELECT COUNT(DISTINCT id)
FROM (
SELECT h.id
FROM host AS h
LEFT JOIN graph_local AS gl
ON h.id = gl.host_id
LEFT JOIN graph_templates AS gt
ON gt.id = gl.graph_template_id
LEFT JOIN host_template AS ht
ON h.host_template_id = ht.id
$sql_where
) AS rower";
}
if (function_exists('get_total_row_data') && $device_id == 0) {
$total_rows = get_total_row_data($user_id, $sql, [], 'thold_device', 10);
} else {
$total_rows = db_fetch_cell($sql);
}
return $host_list;
}
/**
* thold_get_default_template_name - builds a threshold template's default display name from its
* data template and data source names.
*
* @param array $thold_data The threshold row/template providing the name parts.
*
* @return string The composed default name.
*/
function thold_get_default_template_name($thold_data) {
return $thold_data['data_template_name'] . ' [' . $thold_data['data_source_name'] . ']';
}
/**
* thold_get_default_suggested_name - resolves the suggested-name token pattern to use for a
* threshold, preferring an explicitly configured one over the built-in
* `|data_source_description| [|data_source_name|]` default.
*
* @param array $thold_data The threshold/template row providing a 'suggested_name', if set.
* @param int $id Optional threshold template id to look up 'suggested_name' from when
* $thold_data is empty.
*
* @return string The suggested-name token pattern (not yet expanded).
*/
function thold_get_default_suggested_name($thold_data, $id = 0) {
if (empty($thold_data) && $id) {
$thold_data = db_fetch_row_prepared('SELECT suggested_name
FROM thold_template
WHERE id = ?',
[$id]);
}
$desc = '|data_source_description| [|data_source_name|]';
if (isset($thold_data['suggested_name']) && !empty($thold_data['suggested_name'])) {
$desc = $thold_data['suggested_name'];
}
return $desc;
}
/**
* thold_get_cached_name - resolves and caches a threshold's display name, substituting the
* `|data_source_description|` token if the cached name isn't already set.
*
* @param array &$thold_data The threshold row, updated in place with 'name_cache' if it was empty.
*
* @return string The threshold's cached display name.
*/
function thold_get_cached_name(&$thold_data) {
if (empty($thold_data['name_cache'])) {
$thold_data['name_cache'] = thold_substitute_data_source_description($thold_data['name'], $thold_data['local_data_id']);
}
return $thold_data['name_cache'];
}
/**
* Quote a user-supplied value for use as the operand of an RLIKE comparison.
*
* Cacti 1.2.31 added db_qstr_rlike() as the remediation for GHSA-69gg-xrh3-gp82:
* on top of quoting, it caps the operand at 255 bytes and strips the alternation
* and quantifier characters that made the regular expression a denial-of-service
* vector. The plugin still supports 1.2.25, which predates that helper, so fall
* back to plain quoting there.
*
* @param string $value Raw filter value.
*
* @return string RLIKE operator and quoted operand, ready to concatenate.
*/
function thold_rlike_clause($value) {
if (function_exists('db_qstr_rlike')) {
return db_qstr_rlike($value);
}
return 'RLIKE ' . db_qstr($value);
}
/**
* Substitute one tag, rendering an absent value as an empty string.
*
* Only null and false count as absent. Zero is a legitimate reading, and
* blanking it produced alert bodies reading "Current value is " for exactly
* the case an operator most needs to see.
*
* @param string $search Tag to replace.
* @param mixed $replace Value to substitute.
* @param string|null $subject Text containing the tag.
*/
function thold_str_replace(string $search, $replace, ?string $subject): string {
return str_replace($search, $replace ?? '', $subject ?? '');
}
/**
* thold_template_import - imports one or more threshold templates from an XML export, resolving
* the referenced data template/data source by hash, creating or updating the matching
* thold_template row(s) accordingly.
*
* @param string $xml_data The raw XML export data to import.
*
* @global array $config Cacti's global configuration array.
*
* @return array A debug_data array with 'success'/'failure'/'errors' message lists describing the
* outcome of each template processed.
*/
function thold_template_import($xml_data) {
global $config;
$debug_data = [];
if ($xml_data != '') {
// obtain debug information if it's set
$xml_array = xml2array($xml_data);
if (cacti_sizeof($xml_array)) {
foreach ($xml_array as $template => $contents) {
$error = false;
$save = [];
if (cacti_sizeof($contents)) {
foreach ($contents as $name => $value) {
switch($name) {
case 'data_template_id':
case 'data_template_hash':
// See if the hash exists, if it doesn't, Error Out
$found = db_fetch_cell_prepared('SELECT id
FROM data_template
WHERE hash = ?',
[$value]);
if (!empty($found)) {
$save['data_template_id'] = $found;
$save['data_template_hash'] = $value;
} else {
$error = true;
$debug_data['errors'][] = __('Threshold Template Subordinate Data Template Not Found!', 'thold');
}
break;
case 'data_source_id':
// See if the hash exists, if it doesn't, Error Out
$found = db_fetch_cell_prepared('SELECT id
FROM data_template_rrd
WHERE hash = ?',
[$value]);
if (!empty($found)) {
$save['data_source_id'] = $found;
} else {
$error = true;
$debug_data['errors'][] = __('Threshold Template Subordinate Data Source Not Found!', 'thold');
}
break;
case 'hash':
// See if the hash exists, if it does, update the thold
$found = db_fetch_cell_prepared('SELECT id
FROM thold_template
WHERE hash = ?',
[$value]);
if (!empty($found)) {
$save['hash'] = $value;
$save['id'] = $found;
} else {
$save['hash'] = $value;
$save['id'] = 0;
}
break;
case 'name':
$tname = $value;
$save['name'] = $value;
break;
default:
if (db_column_exists('thold_template', $name)) {
$save[$name] = $value;
}
break;
}
}
}
if (!validate_template_import_columns($save)) {
$debug_data['errors'][] = __('Threshold Template import columns do not match the database schema', 'thold');
$error = true;
}
if (!$error) {
$id = sql_save($save, 'thold_template');
if ($id) {
if ($config['is_web']) {
$debug_data['success'][] = __esc('Threshold Template \'%s\' %s!', $tname, ($save['id'] > 0 ? __('Updated', 'thold') : __('Imported', 'thold')), 'thold');
} else {
$debug_data['success'][] = __('Threshold Template \'%s\' %s!', $tname, ($save['id'] > 0 ? __('Updated', 'thold') : __('Imported', 'thold')), 'thold');
}
} else {
if ($config['is_web']) {
$debug_data['failure'][] = __esc('Threshold Template \'%s\' %s Failed!', $tname, ($save['id'] > 0 ? __('Update', 'thold') : __('Import', 'thold')), 'thold');
} else {
$debug_data['failure'][] = __('Threshold Template \'%s\' %s Failed!', $tname, ($save['id'] > 0 ? __('Update', 'thold') : __('Import', 'thold')), 'thold');
}
}
} else {
$debug_data['failure'][] = __('Errors encountered while attempting to import Threshold Template data.', 'thold');
}
}
} else {
$debug_data['failure'][] = __('Threshold Template Import data was not found to be XML data.', 'thold');
}
} else {
$debug_data['failure'][] = __('Threshold Template Import data was not correct while importing Threshold Template.', 'thold');
}
return $debug_data;
}
/**
* validate_template_import_columns - verifies every key in an imported threshold template's save
* array corresponds to a real thold_template column.
*
* @param array $template The threshold template save array to validate.
*
* @return bool True when every column exists (and the array is non-empty); false otherwise.
*/
function validate_template_import_columns($template) {
if (cacti_sizeof($template)) {
foreach ($template as $column => $data) {
if (!db_column_exists('thold_template', $column)) {
cacti_log('Template column \'' . $column . '\' is not valid for a threshold template.', false, 'THOLD');
return false;
}
}
} else {
return false;
}
return true;
}
/**
* thold_error_handler - a PHP error handler that logs errors to the Cacti log (at debug
* verbosity), suppressing a couple of known-benign notices (timezone/reference-related).
*
* @param int $errno The PHP error level constant.
* @param string $errmsg The error message text.
* @param string $filename The file the error occurred in.
* @param int $linenum The line number the error occurred on.
* @param array $vars Unused; retained for compatibility with PHP's error handler signature.
*
* @return void
*/
function thold_error_handler($errno, $errmsg, $filename, $linenum, $vars = []) {
if (read_config_option('log_verbosity') >= POLLER_VERBOSITY_DEBUG) {
// define all error types
$errortype = [
E_ERROR => 'Error',
E_WARNING => 'Warning',
E_PARSE => 'Parsing Error',
E_NOTICE => 'Notice',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User Notice',
E_STRICT => 'Runtime Notice'
];
if (defined('E_RECOVERABLE_ERROR')) {
$errortype[E_RECOVERABLE_ERROR] = 'Catchable Fatal Error';
}
if (defined('E_DEPRECATED')) {
$errortype[E_DEPRECATED] = 'Deprecated Warning';
}
// create an error string for the log
$err = "ERRNO:'" . $errno . "' TYPE:'" . $errortype[$errno] .
"' MESSAGE:'" . $errmsg . "' IN FILE:'" . $filename .
"' LINE NO:'" . $linenum . "'";
// let's ignore some lesser issues
if (substr_count($errmsg, 'date_default_timezone')) {
return;
}
if (substr_count($errmsg, 'Only variables')) {
return;
}
// log the error to the Cacti log
cacti_log('PROGERR: ' . $err, false, 'THOLD');
}
return;
}
/**
* thold_get_rrd_statistics - given the local_data_id, the data source name, the interval and
* consolidation function. Return some statistics to the caller.
*
* @param $local_data_id - (int) The local_data_id of the data source
* @param $current_time - (int) The end time for the RRDtool call
* @param $prev_time - (int) The start time for the RRDtool call
* @param $avg_of_cf - (string) Get the Average of the chosen CF
*
* @return - (boolean|array) The value requested by the caller
*/
function thold_get_rrd_statistics($local_data_id, $current_time, $prev_time, $avg_of_cf) {
global $config;
$use_proxy = (read_config_option('storage_location') ? true : false);
$rrdfile = db_fetch_cell_prepared('SELECT rrd_path
FROM poller_item
WHERE local_data_id = ?',
[$local_data_id]);
if ($rrdfile == '') {
return false;
}
if ($use_proxy) {
$file_exists = rrdtool_execute("file_exists $rrdfile", true, RRDTOOL_OUTPUT_BOOLEAN, false, 'THOLD');
} else {
clearstatcache();
$file_exists = file_exists($rrdfile);
}
// don't attempt to get information if the file does not exist
if ($file_exists) {
$info = rrdtool_execute("info $rrdfile", false, RRDTOOL_OUTPUT_STDOUT, false, 'THOLD');
// don't do anything if RRDfile did not return data
if ($info != '') {
$info_array = explode("\n", $info);
$average = false;
$max = false;
$min = false;
$last = false;
$dsnames = [];
/* figure out what is in this RRDfile. Assume CF Uniformity as Cacti does not allow async rrdfiles.
* also verify the consolidation functions in the RRDfile for average and max calculations.
*/
if (cacti_sizeof($info_array)) {
foreach ($info_array as $line) {
if (substr_count($line, 'ds[')) {
$parts = explode(']', $line);
$parts2 = explode('[', $parts[0]);
$dsnames[trim($parts2[1])] = 1;
} elseif (substr_count($line, '.cf')) {
$parts = explode('=', $line);
if (substr_count($parts[1], 'AVERAGE')) {
$average = true;
} elseif (substr_count($parts[1], 'MAX')) {
$max = true;
} elseif (substr_count($parts[1], 'MIN')) {
$min = true;
} elseif (substr_count($parts[1], 'LAST')) {
$last = true;
}
} elseif (substr_count($line, 'step')) {
$parts = explode('=', $line);
$poller_interval = trim($parts[1]);
}
}
}
// create the command syntax to get data
// assume that an RRDfile has not more than 62 data sources
$defs = 'abcdefghijklmnopqrstuvwxyz012345789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$i = 0;
$j = 0;
$def = '';
$command = '';
$dsvalues = [];
// escape the file name if on Windows
if ($config['cacti_server_os'] != 'unix') {
$rrdfile = str_replace(':', '\\:', $rrdfile);
}
// setup the graph command by parsing through the internal data source names
if (cacti_sizeof($dsnames)) {
foreach ($dsnames as $dsname => $present) {
if ($average) {
$def .= 'DEF:' . $defs[$j] . $defs[$i] . '="' . $rrdfile . '":' . $dsname . ':AVERAGE ';
$command .= ' VDEF:' . $defs[$j] . $defs[$i] . '_out=' . $defs[$j] . $defs[$i] . ',AVERAGE PRINT:' . $defs[$j] . $defs[$i] . '_out:%lf';
$i++;
}
if ($max) {
$def .= 'DEF:' . $defs[$j] . $defs[$i] . '="' . $rrdfile . '":' . $dsname . ':MAX ';
$command .= ' VDEF:' . $defs[$j] . $defs[$i] . '_out=' . $defs[$j] . $defs[$i] . ',MAXIMUM PRINT:' . $defs[$j] . $defs[$i] . '_out:%lf';
$i++;
}
if ($min) {
$def .= 'DEF:' . $defs[$j] . $defs[$i] . '="' . $rrdfile . '":' . $dsname . ':MIN ';
$command .= ' VDEF:' . $defs[$j] . $defs[$i] . '_out=' . $defs[$j] . $defs[$i] . ',MINIMUM PRINT:' . $defs[$j] . $defs[$i] . '_out:%lf';
$i++;
}
if ($last) {
$def .= 'DEF:' . $defs[$j] . $defs[$i] . '="' . $rrdfile . '":' . $dsname . ':LAST ';
$command .= ' VDEF:' . $defs[$j] . $defs[$i] . '_out=' . $defs[$j] . $defs[$i] . ',LAST PRINT:' . $defs[$j] . $defs[$i] . '_out:%lf';
$i++;
}
if ($i > 50) {
$j++;
$i = 0;
}
}
}
if ($avg_of_cf != '') {
switch($avg_of_cf) {
case 'MIN':
if ($min) {
$def .= 'DEF:' . $defs[$j] . $defs[$i] . '="' . $rrdfile . '":' . $dsname . ':MIN ';
$command .= ' VDEF:' . $defs[$j] . $defs[$i] . '_cfa=' . $defs[$j] . $defs[$i] . ',AVERAGE PRINT:' . $defs[$j] . $defs[$i] . '_cfa:%lf';
$i++;
}
break;
case 'MAX':
if ($max) {
$def .= 'DEF:' . $defs[$j] . $defs[$i] . '="' . $rrdfile . '":' . $dsname . ':MAX ';
$command .= ' VDEF:' . $defs[$j] . $defs[$i] . '_cfa=' . $defs[$j] . $defs[$i] . ',AVERAGE PRINT:' . $defs[$j] . $defs[$i] . '_cfa:%lf';
$i++;
}
break;
case 'AVG':
if ($average) {
$def .= 'DEF:' . $defs[$j] . $defs[$i] . '="' . $rrdfile . '":' . $dsname . ':AVERAGE ';
$command .= ' VDEF:' . $defs[$j] . $defs[$i] . '_cfa=' . $defs[$j] . $defs[$i] . ',AVERAGE PRINT:' . $defs[$j] . $defs[$i] . '_cfa:%lf';
$i++;
}
break;
case 'LAST':
if ($last) {
$def .= 'DEF:' . $defs[$j] . $defs[$i] . '="' . $rrdfile . '":' . $dsname . ':LAST ';
$command .= ' VDEF:' . $defs[$j] . $defs[$i] . '_cfa=' . $defs[$j] . $defs[$i] . ',AVERAGE PRINT:' . $defs[$j] . $defs[$i] . '_cfa:%lf';
$i++;
}
break;
}
}
// now execute the graph command
$stats_cmd = 'graph x --start ' . $prev_time . ' --end ' . $current_time . ' ' . trim($def) . ' ' . trim($command);
// uncomment for diagnostics
// cacti_log($stats_cmd, false, 'THOLD');
$xport_data = rrdtool_execute($stats_cmd, false, RRDTOOL_OUTPUT_STDOUT, false, 'THOLD');
$position = [];
$position[] = ['RETURN' => 'RETURN'];
// initialize the array of return values
foreach ($dsnames as $dsname => $present) {
$dsvalues[$dsname]['AVG'] = 0;
$dsvalues[$dsname]['MAX'] = 0;
$dsvalues[$dsname]['MIN'] = 0;
$dsvalues[$dsname]['LAST'] = 0;
if ($avg_of_cf != '') {
$dsvalues[$dsname]['CFA'] = 0;
}
if ($average) {
$position[] = [$dsname => 'AVG'];
}
if ($max) {
$position[] = [$dsname => 'MAX'];
}
if ($min) {
$position[] = [$dsname => 'MIN'];
}
if ($last) {
$position[] = [$dsname => 'LAST'];
}
if ($avg_of_cf != '') {
$position[] = [$dsname => 'CFA'];
}
}
// process the xport array and return average and peak values
if ($xport_data != '') {
$xport_array = explode("\n", $xport_data);
// print_r($xport_array);
if (cacti_sizeof($xport_array)) {
foreach ($xport_array as $index => $line) {
if ($line == '') {
continue;
}
if ($index > 0) {
// Catch the last line
if (substr($line, 0, 2) == 'OK') {
$line = trim($line, ' OK');
$parts = explode(' ', $line);
// print $line . PHP_EOL;
foreach ($parts as $line) {
$sparts = explode(':', $line);
switch($sparts[0]) {
case 'u':
$user_time = $sparts[1];
break;
case 's':
$system_time = $sparts[1];
break;
case 'r':
$real_time = $sparts[1];
break;
}
}
break;
} else {
foreach ($position[$index] as $dsname => $stat) {
$dsvalues[$dsname][$stat] = trim($line);
}
}
}
}
return $dsvalues;
}
}
}
}
return false;
}