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   $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 "
\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 ''; if (!isset($thold_start_rusage)) { print "ERROR: Can not display RUSAGE please call thold_initialize_rusage first"; } 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 ""; print '' . __('Time:', 'thold') . ' ' . round($end_time - $start_time,2) . ' seconds, '; print '' . __('User:', 'thold') . ' ' . round($utime,2) . ' seconds, '; print '' . __('System:', 'thold') . ' ' . round($stime,2) . ' seconds, '; print '' . __('Swaps:', 'thold') . ' ' . ($swaps) . ' swaps, '; print '' . __('Pages:', 'thold') . ' ' . ($pages) . ' pages'; print ''; } print ''; 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 ''; print '
'; foreach ($thold_states as $index => $state) { print '
' . html_escape($state['display']) . '
'; } print '
'; print ''; 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 ''; print '
'; foreach ($thold_host_states as $index => $state) { print '
' . html_escape($state['display']) . '
'; } print '
'; print ''; 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 ''; print '
'; foreach ($thold_log_states as $index => $state) { print '
' . html_escape($state['display_short']) . '
'; } print '
'; print ''; 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 $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 $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 $thold_data * @param array $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 $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 $thold_data * @param array $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 $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 $thold_data * @param array $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 $thold_data * @param array $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_|`-style token to the matching * cached SNMP data-query field value for a device/index, if present. * * @param string $string The raw `|query_|` 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 (, |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_|` 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 $file_array Graph attachment, or empty for none. * @param array $thold_data Threshold row. * @param array $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( ['', ''], [$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( ['', ''], [$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( ['', ''], [$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( ['', ''], [$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('', $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('', $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 ``/``/``/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 = [ '', '', '', '', '', '', '', '', '', '' ]; $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 = [ '', '', '', '', '', '', '', '', '', '' ]; $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 = [ '', '', '', '', '', '', '', '', '', '' ]; $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 `` 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('', $thold_snmp_data['eventSource'], $snmp_event_description); $snmp_event_description = thold_str_replace('', $thold_snmp_data['eventDevice'], $snmp_event_description); $snmp_event_description = thold_str_replace('', ($thold['thold_template_id'] ? $thold['thold_template_id'] : 'none'), $snmp_event_description); $snmp_event_description = thold_str_replace('', (isset($thold['name_cache']) ? $thold['name_cache'] : 'none'), $snmp_event_description); $snmp_event_description = thold_str_replace('', $thold_snmp_data['eventThresholdType'], $snmp_event_description); $snmp_event_description = thold_str_replace('', $thold_snmp_data['eventDataSource'], $snmp_event_description); $snmp_event_description = thold_str_replace('', $thold_snmp_data['eventHigh'], $snmp_event_description); $snmp_event_description = thold_str_replace('', $thold_snmp_data['eventLow'], $snmp_event_description); $snmp_event_description = thold_str_replace('', $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 `` 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 `` 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 of "") can't be substituted a second time // outside of $q()'s quoting. $replacements = [ '' => $q($h['description']), '' => $q($h['hostname']), '' => $q($h['location']), '' => $q($site), '' => $local_graph_id, '' => $thold['id'], '' => $q($currentval), '' => $q($thold['name_cache']), '' => $q($data_source_name), '' => $q($thold['notes']), '' => $q($thold['dnotes']), '' => $q($thold['dnotes']), '' => $q($thold['external_id']), '