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

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

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

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

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

* Let unexpected errors fail the maint-guard tests

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

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

---------

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

986 lines
40 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDTool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
/**
* Launches the Thold poller script (poller_thold.php) as a background
* process, logging a fatal error instead if the script file is
* missing. Registered as the 'poller_bottom' Cacti hook, invoked at
* the end of each Cacti poller cycle.
*
* @return void
*
* @global array $config Cacti global configuration array;
* used to build the poller script's
* path and command line.
* @global string $database_type Reserved/declared for parity with
* other functions in this file; not
* used directly here.
* @global string $database_default Reserved/declared for parity with
* other functions in this file; not
* used directly here.
* @global string $database_hostname Reserved/declared for parity with
* other functions in this file; not
* used directly here.
* @global string $database_username Reserved/declared for parity with
* other functions in this file; not
* used directly here.
* @global string $database_password Reserved/declared for parity with
* other functions in this file; not
* used directly here.
* @global int $database_port Reserved/declared for parity with
* other functions in this file; not
* used directly here.
* @global bool $database_ssl Reserved/declared for parity with
* other functions in this file; not
* used directly here.
*/
function thold_poller_bottom() {
global $config, $database_type, $database_default, $database_hostname;
global $database_username, $database_password, $database_port, $database_ssl;
$command_string = cacti_escapeshellcmd(read_config_option('path_php_binary'));
$file_path = $config['base_path'] . '/plugins/thold/poller_thold.php';
if (file_exists($file_path)) {
exec_background($command_string, $file_path);
} else {
cacti_log('FATAL: The Threshold Processing Script \'poller_thold.php\' is missing', false, 'THOLD');
}
}
/**
* Deletes plugin_thold_log entries older than the configured retention
* period (thold_log_storage days, defaulting to and persisting 31 if
* unset), and updates the last-change timestamp setting when any rows
* were actually removed.
*
* @return void
*/
function thold_cleanup_log() {
$daysToStoreLogs = read_config_option('thold_log_storage');
if (empty($daysToStoreLogs)) {
$daysToStoreLogs = 31;
set_config_option('thold_log_storage', '31');
}
$t = time() - (86400 * $daysToStoreLogs); // Delete Logs over a month old
db_execute_prepared('DELETE FROM plugin_thold_log
WHERE time < ?',
[$t]);
if (db_affected_rows() > 0) {
set_config_option('time_last_change_thold_log', time());
}
}
/**
* Cacti poller output-processing hook: intercepts newly collected RRD
* data source values, re-indexes them by data source id for threshold
* evaluation, and hands them off to the threshold-checking logic
* before/alongside the normal RRD update flow. Registered as a Cacti
* poller output hook.
*
* @param array &$rrd_update_array The newly collected poller output
* data, keyed by local_data_id.
*
* @return array The (possibly filtered/annotated) $rrd_update_array,
* for hook chaining.
*
* @global array $config Cacti global configuration array; used to
* locate library files to include.
* @global bool $debug Whether debug output is enabled.
*/
function thold_poller_output(&$rrd_update_array) {
global $config, $debug;
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include_once($config['library_path'] . '/snmp.php');
$rrd_reindexed = [];
$rrd_time_reindexed = [];
$local_data_ids = '';
foreach ($rrd_update_array as $item) {
if (isset($item['times'][key($item['times'])])) {
$local_data_ids .= ($local_data_ids != '' ? ', ' : '') . $item['local_data_id'];
$rrd_reindexed[$item['local_data_id']] = $item['times'][key($item['times'])];
$rrd_time_reindexed[$item['local_data_id']] = key($item['times']);
}
}
if ($local_data_ids != '') {
if (read_config_option('thold_daemon_enable') == 'on') {
// array_chunk() takes the size of each chunk, not how many to make.
$rrd_update_array_chunks = array_chunk($rrd_update_array, 50, true);
foreach ($rrd_update_array_chunks as $rrd_update_array_chunk) {
$rrd_reindexed = [];
$rrd_time_reindexed = [];
$local_data_ids = '';
$thold_items = [];
foreach ($rrd_update_array_chunk as $item) {
if (isset($item['times'][key($item['times'])])) {
$local_data_ids .= ($local_data_ids != '' ? ', ' : '') . $item['local_data_id'];
$rrd_reindexed[$item['local_data_id']] = $item['times'][key($item['times'])];
$rrd_time_reindexed[$item['local_data_id']] = key($item['times']);
}
}
if ($local_data_ids != '') {
$thold_items = db_fetch_assoc("SELECT id, local_data_id, thread_id
FROM thold_data
WHERE thold_data.local_data_id IN ($local_data_ids)");
}
if (cacti_sizeof($thold_items)) {
/**
* cache required polling data. prefer bulk inserts for
* performance reasons - start with chunks of 1000 items
*/
$sql_max_inserts = 1000;
$thold_items = array_chunk($thold_items, $sql_max_inserts);
$sql_insert = 'INSERT INTO plugin_thold_daemon_data
(poller_id, id, thread_id, rrd_reindexed, rrd_time_reindexed) VALUES ';
foreach ($thold_items as $packet) {
$sql_values = '';
foreach ($packet as $thold_item) {
$sql_values .= ($sql_values != '' ? ', ' : '') . '(' . $config['poller_id'] . ', ' . $thold_item['id'] . ', ' . $thold_item['thread_id'] . ', ' . db_qstr(json_encode($rrd_reindexed[$thold_item['local_data_id']])) . ', ' . $rrd_time_reindexed[$thold_item['local_data_id']] . ')';
}
db_execute($sql_insert . $sql_values);
}
}
}
return $rrd_update_array;
}
} else {
return $rrd_update_array;
}
$tholds = db_fetch_assoc("SELECT td.id, td.name_cache AS thold_name,
td.local_graph_id, td.percent_ds, td.expression, td.upper_ds, td.data_type,
td.cdef, td.local_data_id, td.data_template_rrd_id, td.lastread,
UNIX_TIMESTAMP(td.lasttime) AS lasttime, td.oldvalue,
td.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 IN($local_data_ids)");
if (cacti_sizeof($tholds)) {
$sql = [];
$status_sql = [];
foreach ($tholds as $thold_data) {
thold_debug("Checking Threshold: Name: '" . $thold_data['thold_name'] . "', Graph: '" . $thold_data['local_graph_id'] . "'");
$item = [];
$currenttime = 0;
$postvalue = thold_get_currentval($thold_data, $rrd_reindexed, $rrd_time_reindexed, $item, $currenttime);
$currentval = $postvalue;
switch ($thold_data['data_type']) {
case 0:
break;
case 1:
if ($thold_data['cdef'] != 0) {
$currentval = thold_build_cdef($thold_data['cdef'], $postvalue, $thold_data['local_data_id'], $thold_data['data_template_rrd_id']);
}
break;
case 2:
if ($thold_data['percent_ds'] != '') {
$currentval = thold_calculate_percent($thold_data, $postvalue, $rrd_reindexed);
}
break;
case 3:
if ($thold_data['expression'] != '') {
$currentval = thold_calculate_expression($thold_data, $postvalue, $rrd_reindexed, $rrd_time_reindexed);
}
break;
case 4:
if ($thold_data['upper_ds'] != '') {
$currentval = thold_calculate_lower_upper($thold_data, $postvalue, $rrd_reindexed);
}
break;
}
if (!is_numeric($currentval)) {
if (read_config_option('thold_consider_unknown_zero') == 'on') {
$normalized = strtolower($currentval);
if ($normalized == 'u' || $normalized == 'nan' || $normalized == '') {
// Fail closed: an unavailable sample must not be persisted as a
// manufactured zero, or thold_check_threshold()'s unavailable-sample
// guard never sees it and can alert/recover on the fabricated value.
thold_debug('Threshold: ' . $thold_data['thold_name'] . ' unknown value would have been treated as zero; preserving unavailable state instead', 'thold');
if (read_config_option('thold_log_unknown_to_zero') == 'on') {
cacti_log('NOTE: Threshold \'' . $thold_data['thold_name'] . '\' unknown value would have been treated as zero; preserving unavailable state instead', true, 'THOLD');
}
}
}
$currentval = '';
}
$sample_rows = thold_polling_sample_row($thold_data, $item, $currentval, $currenttime);
if ($sample_rows['sample_row'] !== null) {
$sql[] = $sample_rows['sample_row'];
} elseif ($sample_rows['status_row'] !== null) {
$status_sql[] = $sample_rows['status_row'];
}
}
if (cacti_sizeof($sql)) {
foreach (array_chunk($sql, 400) as $chunk) {
$placeholders = implode(', ', array_fill(0, cacti_sizeof($chunk), '(?, ?, ?, FROM_UNIXTIME(?), ?)'));
$params = [];
foreach ($chunk as $row) {
$params[] = $row['id'];
$params[] = $row['tcheck'];
$params[] = $row['lastread'];
$params[] = $row['lasttime'];
$params[] = $row['oldvalue'];
}
db_execute_prepared('INSERT INTO thold_data
(id, tcheck, lastread, lasttime, oldvalue)
VALUES ' . $placeholders . '
ON DUPLICATE KEY UPDATE
tcheck = VALUES(tcheck),
lastread = VALUES(lastread),
lasttime = VALUES(lasttime),
oldvalue = VALUES(oldvalue)',
$params);
}
}
if (cacti_sizeof($status_sql)) {
foreach (array_chunk($status_sql, 400) as $chunk) {
$placeholders = implode(', ', array_fill(0, cacti_sizeof($chunk), '(?, ?, ?)'));
$params = [];
foreach ($chunk as $row) {
$params[] = $row['id'];
$params[] = $row['tcheck'];
$params[] = $row['lastread'];
}
db_execute_prepared('INSERT INTO thold_data
(id, tcheck, lastread)
VALUES ' . $placeholders . '
ON DUPLICATE KEY UPDATE
tcheck = VALUES(tcheck),
lastread = VALUES(lastread)',
$params);
}
}
thold_polling_cleanup(cacti_sizeof($sql) || cacti_sizeof($status_sql));
}
return $rrd_update_array;
}
/**
* Evaluates every configured threshold against its data source's
* current/historical values (fetching remote-poller data directly from
* the remote database when remote storage is configured), triggering
* notifications/alerts for thresholds that have tripped or recovered.
* Invoked once per poller cycle from the main threshold-checking flow.
*
* @return int The total number of thresholds evaluated.
*
* @global array $config Cacti global configuration array; used to
* locate library files to include.
*/
function thold_check_all_thresholds() {
global $config;
include($config['base_path'] . '/plugins/thold/includes/arrays.php');
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include_once($config['base_path'] . '/lib/time.php');
if (read_config_option('remote_storage_method') == 1) {
if ($config['poller_id'] == 1) {
$sql_query = "SELECT td.*, h.hostname,
h.description, h.site_id, h.location, h.notes AS dnotes, h.snmp_engine_id
FROM thold_data AS td
LEFT JOIN host AS h
ON h.id = td.host_id
LEFT JOIN data_template_rrd AS dtr
ON dtr.id = td.data_template_rrd_id
WHERE thold_per_enabled = 'on'
AND (
thold_template_id = 0
OR (
thold_template_id > 0
AND (
template_enabled != 'on' OR (template_enabled = 'on' AND thold_enabled = 'on')
)
)
)
AND (h.poller_id = 1 OR h.poller_id IS NULL)
AND td.tcheck = 1
AND h.status = 3";
} else {
$sql_query = 'SELECT td.*, h.hostname,
h.description, h.site_id, h.location, h.notes AS dnotes, h.snmp_engine_id
FROM thold_data AS td
LEFT JOIN host AS h
ON h.id = td.host_id
LEFT JOIN data_template_rrd AS dtr
ON dtr.id = td.data_template_rrd_id
WHERE h.poller_id = ' . $config['poller_id'] . "
AND thold_per_enabled = 'on'
AND (
thold_template_id = 0
OR (
thold_template_id > 0
AND (
template_enabled != 'on' OR (template_enabled = 'on' AND thold_enabled = 'on')
)
)
)
AND td.tcheck = 1
AND h.status = 3";
}
} else {
$sql_query = "SELECT td.*, h.hostname,
h.description, h.site_id, h.location, h.notes AS dnotes, h.snmp_engine_id
FROM thold_data AS td
LEFT JOIN data_template_rrd AS dtr
ON dtr.id = td.data_template_rrd_id
LEFT JOIN host as h
ON td.host_id = h.id
WHERE thold_per_enabled = 'on'
AND (
thold_template_id = 0
OR (
thold_template_id > 0
AND (
template_enabled != 'on' OR (template_enabled = 'on' AND thold_enabled = 'on')
)
)
)
AND td.tcheck = 1
AND h.status = 3";
}
$tholds = api_plugin_hook_function('thold_get_live_hosts', db_fetch_assoc($sql_query));
$total_tholds = sizeof($tholds);
$checked = [];
foreach ($tholds as $thold) {
thold_check_threshold($thold);
$checked[] = (int) $thold['id'];
}
/*
* Clear the dirty flag on what was evaluated, and nothing else. Clearing
* by poller, or across the whole table, also cleared the thresholds whose
* data arrived after the select above, so those readings were dropped
* without ever being checked. The select already scopes to this poller,
* so the identifiers carry that scoping with them.
*/
if (cacti_sizeof($checked)) {
$placeholders = implode(', ', array_fill(0, cacti_sizeof($checked), '?'));
db_execute_prepared('UPDATE thold_data
SET tcheck = 0
WHERE id IN (' . $placeholders . ')',
$checked);
}
return $total_tholds;
}
/**
* Checks each monitored host's reachability status (including hosts
* managed by the Maint plugin, when enabled) and records/clears dead-
* host notification tracking rows, when the alert_deadnotify setting
* is enabled; a no-op otherwise.
*
* @return int|null 0 immediately if dead-host notifications are
* disabled; otherwise no explicit return value
* (null).
*
* @global array $config Cacti global configuration array; used to
* locate library files to include.
*/
function thold_update_host_status() {
global $config;
// Return if we aren't set to notify
$deadnotify = (read_config_option('alert_deadnotify') == 'on');
if (!$deadnotify) {
return 0;
}
include_once($config['base_path'] . '/plugins/thold/includes/functions.php');
include_once($config['library_path'] . '/snmp.php');
if (api_plugin_is_enabled('maint')) {
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');
}
}
$ping_failure_count = read_config_option('ping_failure_count');
$queue = read_config_option('thold_notification_queue');
// Lets find hosts that were down, but are now back up
if (read_config_option('remote_storage_method') == 1) {
$failed = db_fetch_assoc_prepared('SELECT *
FROM plugin_thold_host_failed
WHERE poller_id = ?',
[$config['poller_id']]);
} else {
$failed = db_fetch_assoc('SELECT *
FROM plugin_thold_host_failed');
}
if (cacti_sizeof($failed)) {
foreach ($failed as $fh) {
$alert_email = read_config_option('alert_email');
$host = db_fetch_row_prepared('SELECT *
FROM host
WHERE id = ?',
[$fh['host_id']]);
$notify_list_id = 0;
if (!sizeof($host)) {
db_execute_prepared('DELETE
FROM plugin_thold_host_failed
WHERE host_id = ?',
[$fh['host_id']]);
set_config_option('time_last_change_thold_device', time());
} elseif ($host['status'] == HOST_UP) {
$snmp_system = '';
$snmp_hostname = '';
$snmp_location = '';
$snmp_contact = '';
$snmp_uptime = '';
$uptimelong = '';
$downtimemsg = '';
if (($host['snmp_community'] == '' && $host['snmp_username'] == '') || $host['snmp_version'] == 0) {
// SNMP not in use
$snmp_system = __('SNMP not in use', 'thold');
} else {
$snmp_system = cacti_snmp_get($host['hostname'], $host['snmp_community'], '.1.3.6.1.2.1.1.1.0', $host['snmp_version'], $host['snmp_username'], $host['snmp_password'], $host['snmp_auth_protocol'], $host['snmp_priv_passphrase'], $host['snmp_priv_protocol'], $host['snmp_context'], $host['snmp_port'], $host['snmp_timeout'], read_config_option('snmp_retries'), SNMP_WEBUI);
if (substr_count($snmp_system, '00:')) {
$snmp_system = str_replace('00:', '', $snmp_system);
$snmp_system = str_replace(':', ' ', $snmp_system);
}
if ($snmp_system != '') {
$snmp_uptime = cacti_snmp_get($host['hostname'], $host['snmp_community'], '.1.3.6.1.6.3.10.2.1.3.0', $host['snmp_version'], $host['snmp_username'], $host['snmp_password'], $host['snmp_auth_protocol'], $host['snmp_priv_passphrase'], $host['snmp_priv_protocol'], $host['snmp_context'], $host['snmp_port'], $host['snmp_timeout'], read_config_option('snmp_retries'), SNMP_WEBUI);
if (!is_numeric($snmp_uptime)) {
$snmp_uptime = cacti_snmp_get($host['hostname'], $host['snmp_community'], '.1.3.6.1.2.1.1.3.0', $host['snmp_version'], $host['snmp_username'], $host['snmp_password'], $host['snmp_auth_protocol'], $host['snmp_priv_passphrase'], $host['snmp_priv_protocol'], $host['snmp_context'], $host['snmp_port'], $host['snmp_timeout'], read_config_option('snmp_retries'), SNMP_WEBUI);
} else {
$snmp_uptime *= 100;
}
$snmp_hostname = cacti_snmp_get($host['hostname'], $host['snmp_community'], '.1.3.6.1.2.1.1.5.0', $host['snmp_version'], $host['snmp_username'], $host['snmp_password'], $host['snmp_auth_protocol'], $host['snmp_priv_passphrase'], $host['snmp_priv_protocol'], $host['snmp_context'], $host['snmp_port'], $host['snmp_timeout'], read_config_option('snmp_retries'), SNMP_WEBUI);
$snmp_location = cacti_snmp_get($host['hostname'], $host['snmp_community'], '.1.3.6.1.2.1.1.6.0', $host['snmp_version'], $host['snmp_username'], $host['snmp_password'], $host['snmp_auth_protocol'], $host['snmp_priv_passphrase'], $host['snmp_priv_protocol'], $host['snmp_context'], $host['snmp_port'], $host['snmp_timeout'], read_config_option('snmp_retries'), SNMP_WEBUI);
$snmp_contact = cacti_snmp_get($host['hostname'], $host['snmp_community'], '.1.3.6.1.2.1.1.4.0', $host['snmp_version'], $host['snmp_username'], $host['snmp_password'], $host['snmp_auth_protocol'], $host['snmp_priv_passphrase'], $host['snmp_priv_protocol'], $host['snmp_context'], $host['snmp_port'], $host['snmp_timeout'], read_config_option('snmp_retries'), SNMP_WEBUI);
if (is_numeric($snmp_uptime)) {
$days = intval($snmp_uptime / (60 * 60 * 24 * 100));
$remainder = $snmp_uptime % (60 * 60 * 24 * 100);
$hours = intval($remainder / (60 * 60 * 100));
$remainder = $remainder % (60 * 60 * 100);
$minutes = intval($remainder / (60 * 100));
$uptimelong = $days . 'd ' . $hours . 'h ' . $minutes . 'm';
} else {
$days = '0';
$remainder = '0';
$hours = '0';
$remainder = '0';
$minutes = '0';
$uptimelong = $days . 'd ' . $hours . 'h ' . $minutes . 'm';
}
}
}
if (strtotime($host['status_fail_date']) > 192800) {
$downtime = time() - strtotime($host['status_fail_date']);
$downtime_days = floor($downtime / 86400);
$downtime_hours = floor(($downtime - ($downtime_days * 86400)) / 3600);
$downtime_minutes = floor(($downtime - ($downtime_days * 86400) - ($downtime_hours * 3600)) / 60);
$downtime_seconds = $downtime - ($downtime_days * 86400) - ($downtime_hours * 3600) - ($downtime_minutes * 60);
if ($downtime_days > 0) {
$downtimemsg = $downtime_days . 'd ' . $downtime_hours . 'h ' . $downtime_minutes . 'm ' . $downtime_seconds . 's ';
} elseif ($downtime_hours > 0) {
$downtimemsg = $downtime_hours . 'h ' . $downtime_minutes . 'm ' . $downtime_seconds . 's';
} elseif ($downtime_minutes > 0) {
$downtimemsg = $downtime_minutes . 'm ' . $downtime_seconds . 's';
} else {
$downtimemsg = $downtime_seconds . 's ';
}
} else {
$downtimemsg = __('N/A', 'thold');
}
$subject = read_config_option('thold_up_subject');
if ($subject == '') {
$subject = __('Devices Notice: <DESCRIPTION> (<HOSTNAME>) returned from DOWN state', 'thold');
}
$site = db_fetch_cell_prepared('SELECT name FROM sites WHERE id = ?', [$host['site_id']]);
if ($site == '') {
$site = 'Unspecified';
}
$subject = thold_str_replace('<HOSTNAME>', $host['hostname'], $subject);
$subject = thold_str_replace('<DESCRIPTION>', $host['description'], $subject);
$subject = thold_str_replace('<SITE>', $site, $subject);
$subject = thold_str_replace('<LOCATION>', $host['location'], $subject);
$subject = thold_str_replace('<DOWN/UP>', 'UP', $subject);
$subject = thold_str_replace('<DOWNTIME>', $downtimemsg, $subject);
$subject = thold_str_replace('<NOTES>', $host['notes'], $subject);
$subject = strip_tags($subject);
$msg = read_config_option('thold_up_text');
if ($msg == '') {
$msg = __('<br>System <DESCRIPTION> (<HOSTNAME>) status: <DOWN/UP><br><br>Site: <SITE><br>Location: <LOCATION><br>Current ping response: <CUR_TIME> ms<br>Average system response : <AVG_TIME> ms<br>System availability: <AVAILABILITY><br>Total Checks Since Clear: <TOT_POLL><br>Total Failed Checks: <FAIL_POLL><br>Last Date Checked UP: <LAST_FAIL><br>Devices Previously DOWN for: <DOWNTIME><br><br>SNMP Info:<br>Name - <SNMP_HOSTNAME><br>Location - <SNMP_LOCATION><br>Uptime - <UPTIMETEXT> (<UPTIME> ms)<br>System - <SNMP_SYSTEM><br><br>NOTE: <NOTES>', 'thold');
}
$msg = thold_str_replace('<SUBJECT>', $subject, $msg);
$msg = thold_str_replace('<HOSTNAME>', $host['hostname'], $msg);
$msg = thold_str_replace('<HOST_ID>', $host['id'], $msg);
$msg = thold_str_replace('<DESCRIPTION>', $host['description'], $msg);
$msg = thold_str_replace('<SITE>', $site, $msg);
$msg = thold_str_replace('<LOCATION>', $host['location'], $msg);
$msg = thold_str_replace('<UPTIME>', $snmp_uptime, $msg);
$msg = thold_str_replace('<UPTIMETEXT>', $uptimelong, $msg);
$msg = thold_str_replace('<TIME>', time(), $msg);
$msg = thold_str_replace('<DATE>', date(CACTI_DATE_TIME_FORMAT), $msg);
$msg = thold_str_replace('<DATE_RFC822>', date(DATE_RFC822), $msg);
$msg = thold_str_replace('<DOWNTIME>', $downtimemsg, $msg);
$msg = thold_str_replace('<MESSAGE>', '', $msg);
$msg = thold_str_replace('<DOWN/UP>', 'UP', $msg);
$msg = thold_str_replace('<SNMP_HOSTNAME>', $snmp_hostname, $msg);
$msg = thold_str_replace('<SNMP_LOCATION>', $snmp_location, $msg);
$msg = thold_str_replace('<SNMP_CONTACT>', $snmp_contact, $msg);
$msg = thold_str_replace('<SNMP_SYSTEM>', html_split_string($snmp_system), $msg);
$msg = thold_str_replace('<LAST_FAIL>', $host['status_fail_date'], $msg);
$msg = thold_str_replace('<AVAILABILITY>', number_format_i18n(($host['availability']), 2) . ' %', $msg);
$msg = thold_str_replace('<TOT_POLL>', number_format_i18n($host['total_polls']), $msg);
$msg = thold_str_replace('<FAIL_POLL>', number_format_i18n($host['failed_polls']), $msg);
$msg = thold_str_replace('<CUR_TIME>', number_format_i18n(($host['cur_time']), 2), $msg);
$msg = thold_str_replace('<AVG_TIME>', number_format_i18n(($host['avg_time']), 2), $msg);
$msg = thold_str_replace('<NOTES>', $host['notes'], $msg);
$msg = thold_str_replace("\n", '<br>', $msg);
if (api_plugin_is_enabled('maint') && function_exists('plugin_maint_check_cacti_host') && plugin_maint_check_cacti_host($host['id'])) {
cacti_log('WARNING: Device[' . $host['id'] . '] Hostname[' . $host['hostname'] . '] is recovering! Only logging, maint device', true, 'THOLD');
} else {
switch ($host['thold_send_email']) {
case '0': // Disabled
$alert_email = '';
break;
case '1': // Global List
break;
case '2': // Devices List Only
$alert_email = get_thold_notification_emails($host['thold_host_email']);
$notify_list_id = $host['thold_host_email'];
break;
case '3': // Global and Devices List
$alert_email = $alert_email . ',' . get_thold_notification_emails($host['thold_host_email']);
$notify_list_id = $host['thold_host_email'];
break;
}
api_plugin_hook_function(
'thold_device_recovering',
[
'device' => $host,
'subject' => $subject,
'message' => $msg,
'email' => $alert_email
]
);
cacti_log('WARNING: Device[' . $host['id'] . '] Hostname[' . $host['hostname'] . '] is recovering!', true, 'THOLD');
if ($alert_email == '' && $host['thold_send_email'] > 0) {
cacti_log('WARNING: Device[' . $host['id'] . '] Hostname[' . $host['hostname'] . '] can not send a Device recovering email for \'' . $host['description'] . '\' since the \'Alert Email\' setting is not set for Device!', true, 'THOLD');
} elseif ($host['thold_send_email'] == '0') {
cacti_log('NOTE: Device[' . $host['id'] . '] Hostname[' . $host['hostname'] . '] did not send a Device recovering email for \'' . $host['description'] . '\', disabled per Device setting!', true, 'THOLD');
} elseif ($alert_email != '') {
thold_mail($alert_email, '', '', $subject, $msg, '', '', $notify_list_id, $host, '', 7, 'thold_uhost_mail');
}
$command = read_config_option('thold_device_command');
if ($command != '') {
thold_putenv('THOLD_SUBJECT=' . $subject, true);
thold_putenv('THOLD_HOSTNAME=' . $host['hostname']);
thold_putenv('THOLD_HOST_ID=' . $host['id']);
thold_putenv('THOLD_DESCRIPTION=' . $host['description']);
thold_putenv('THOLD_SITE=' . $site);
thold_putenv('THOLD_LOCATION=' . $host['location']);
thold_putenv('THOLD_TIME=' . time());
thold_putenv('THOLD_DATE=' . date(CACTI_DATE_TIME_FORMAT));
thold_putenv('THOLD_DATE_RFC822=' . date(DATE_RFC822));
thold_putenv('THOLD_UPTIME=' . $snmp_uptime);
thold_putenv('THOLD_UPTIMETEXT=' . $uptimelong);
thold_putenv('THOLD_DOWNTIME=' . $downtimemsg);
thold_putenv('THOLD_MESSAGE=' . '');
thold_putenv('THOLD_DOWNUP=' . 'UP');
thold_putenv('THOLD_SNMP_HOSTNAME=' . $snmp_hostname);
thold_putenv('THOLD_SNMP_LOCATION=' . $snmp_location);
thold_putenv('THOLD_SNMP_CONTACT=' . $snmp_contact);
thold_putenv('THOLD_SNMP_SYSTEM=' . $snmp_system);
thold_putenv('THOLD_LAST_FAIL=' . $host['status_fail_date']);
thold_putenv('THOLD_AVAILABILITY=' . $host['availability']);
thold_putenv('THOLD_TOT_POLL=' . $host['total_polls']);
thold_putenv('THOLD_FAIL_POLL=' . $host['failed_polls']);
thold_putenv('THOLD_CUR_TIME=' . $host['cur_time']);
thold_putenv('THOLD_AVG_TIME=' . $host['avg_time']);
$environment = thold_putenv('THOLD_NOTES=' . $host['notes']);
if (file_exists($command) && is_executable($command)) {
$output = [];
$return = 0;
if ($queue == '') {
exec($command, $output, $return);
cacti_log('Device Up Command for Device[' . $host['id'] . '] Command[' . $command . '] ExitStatus[' . $return . '] Output[' . implode(' ', $output) . ']', false, 'THOLD');
} else {
$data = [
'command' => $command,
'data' => $host,
'environment' => $environment
];
thold_notification_add('thold_uhost_cmd', $data, 'id', $notify_list_id, $host);
}
} else {
cacti_log('WARNING: Device Up Command for Device[' . $host['id'] . '] Command[' . $command . '] Is either Not found or Not executable!', false, 'THOLD');
}
}
}
}
}
}
// Lets find hosts that are down
if (read_config_option('remote_storage_method') == 1) {
$hosts = db_fetch_assoc_prepared('SELECT *
FROM host
WHERE disabled = ""
AND availability_method > 0
AND status = ?
AND status_event_count = IF(thold_failure_count > 0, thold_failure_count, ?)
AND poller_id = ?',
[HOST_DOWN, $ping_failure_count, $config['poller_id']]);
} else {
$hosts = db_fetch_assoc_prepared('SELECT *
FROM host
WHERE disabled = ""
AND availability_method > 0
AND status = ?
AND status_event_count = IF(thold_failure_count > 0, thold_failure_count, ?)',
[HOST_DOWN, $ping_failure_count]);
}
$total_hosts = cacti_sizeof($hosts);
if ($total_hosts) {
foreach ($hosts as $host) {
$notify_list_id = 0;
$alert_email = read_config_option('alert_email');
if (api_plugin_is_enabled('maint') && function_exists('plugin_maint_check_cacti_host') && plugin_maint_check_cacti_host($host['id'])) {
cacti_log('WARNING: Device[' . $host['id'] . '] Hostname[' . $host['hostname'] . '] is DOWN. Only logging, maint device', true, 'THOLD');
} else {
if (strtotime($host['status_fail_date']) > 192800 || strtotime($host['status_rec_date']) > 192800) {
$down_sec = db_fetch_cell_prepared('SELECT unix_timestamp(status_rec_date)-unix_timestamp(status_fail_date)
FROM host WHERE id = ?',
[$host['id']]);
$days = intval($down_sec / (60 * 60 * 24));
$remainder = $down_sec % (60 * 60 * 24);
$hours = intval($remainder / (60 * 60));
$remainder = $remainder % (60 * 60);
$minutes = intval($remainder / (60));
$last_state_duration = $days . 'd ' . $hours . 'h ' . $minutes . 'm';
} else {
$last_state_duration = __('N/A', 'thold');
}
$subject = read_config_option('thold_down_subject');
if ($subject == '') {
$subject = __('Devices Error: <DESCRIPTION> (<HOSTNAME>) is DOWN', 'thold');
}
$site = db_fetch_cell_prepared('SELECT name FROM sites WHERE id = ?', [$host['site_id']]);
if ($site == '') {
$site = 'Unspecified';
}
$subject = thold_str_replace('<HOSTNAME>', $host['hostname'], $subject);
$subject = thold_str_replace('<DESCRIPTION>', $host['description'], $subject);
$subject = thold_str_replace('<SITE>', $site, $subject);
$subject = thold_str_replace('<LOCATION>', $host['location'], $subject);
$subject = thold_str_replace('<DOWN/UP>', __('DOWN', 'thold'), $subject);
$subject = thold_str_replace('<DOWNTIME>', $last_state_duration, $subject);
$subject = thold_str_replace('<NOTES>', $host['notes'], $subject);
$subject = strip_tags($subject);
$msg = read_config_option('thold_down_text');
if ($msg == '') {
$msg = __('System Error : <DESCRIPTION> (<HOSTNAME>) is <DOWN/UP><br>Site: <SITE><br>Location: <LOCATION><br>Reason: <MESSAGE><br><br>Average system response : <AVG_TIME> ms<br>System availability: <AVAILABILITY><br>Total Checks Since Clear: <TOT_POLL><br>Total Failed Checks: <FAIL_POLL><br>Last Date Checked DOWN : <LAST_FAIL><br>Devices Previously UP for: <DOWNTIME><br>NOTE: <NOTES>', 'thold');
}
$msg = thold_str_replace('<SUBJECT>', $subject, $msg);
$msg = thold_str_replace('<HOSTNAME>', $host['hostname'], $msg);
$msg = thold_str_replace('<HOST_ID>', $host['id'], $msg);
$msg = thold_str_replace('<DESCRIPTION>', $host['description'], $msg);
$msg = thold_str_replace('<SITE>', $site, $msg);
$msg = thold_str_replace('<LOCATION>', $host['location'], $msg);
$msg = thold_str_replace('<UPTIME>', '', $msg);
$msg = thold_str_replace('<DOWNTIME>', $last_state_duration, $msg);
$msg = thold_str_replace('<TIME>', time(), $msg);
$msg = thold_str_replace('<DATE>', date(CACTI_DATE_TIME_FORMAT), $msg);
$msg = thold_str_replace('<DATE_RFC822>', date(DATE_RFC822), $msg);
$msg = thold_str_replace('<MESSAGE>', $host['status_last_error'], $msg);
$msg = thold_str_replace('<DOWN/UP>', __('DOWN', 'thold'), $msg);
$msg = thold_str_replace('<SNMP_HOSTNAME>', '', $msg);
$msg = thold_str_replace('<SNMP_LOCATION>', '', $msg);
$msg = thold_str_replace('<SNMP_CONTACT>', '', $msg);
$msg = thold_str_replace('<SNMP_SYSTEM>', '', $msg);
$msg = thold_str_replace('<LAST_FAIL>', $host['status_fail_date'], $msg);
$msg = thold_str_replace('<AVAILABILITY>', round(($host['availability']), 2) . ' %', $msg);
$msg = thold_str_replace('<CUR_TIME>', round(($host['cur_time']), 2), $msg);
$msg = thold_str_replace('<TOT_POLL>', $host['total_polls'], $msg);
$msg = thold_str_replace('<FAIL_POLL>', $host['failed_polls'], $msg);
$msg = thold_str_replace('<AVG_TIME>', round(($host['avg_time']), 2), $msg);
$msg = thold_str_replace('<NOTES>', $host['notes'], $msg);
$msg = thold_str_replace("\n", '<br>', $msg);
switch ($host['thold_send_email']) {
case '0': // Disabled
$alert_email = '';
break;
case '1': // Global List
break;
case '2': // Devices List Only
$alert_email = get_thold_notification_emails($host['thold_host_email']);
$notify_list_id = $host['thold_host_email'];
break;
case '3': // Global and Devices List
$alert_email = $alert_email . ',' . get_thold_notification_emails($host['thold_host_email']);
$notify_list_id = $host['thold_host_email'];
break;
}
api_plugin_hook_function(
'thold_device_down',
[
'device' => $host,
'subject' => $subject,
'message' => $msg,
'email' => $alert_email
]
);
cacti_log('WARNING: Device[' . $host['id'] . '] Hostname[' . $host['hostname'] . '] is down!', true, 'THOLD');
if ($alert_email == '' && $host['thold_send_email'] > 0) {
cacti_log('WARNING: Device[' . $host['id'] . '] Hostname[' . $host['hostname'] . '] can not send a Device down email for \'' . $host['description'] . '\' since the \'Alert Email\' setting is not set for Device!', true, 'THOLD');
} elseif ($host['thold_send_email'] == '0') {
cacti_log('NOTE: Device[' . $host['id'] . '] Hostname[' . $host['hostname'] . '] did not send a Device down email for \'' . $host['description'] . '\', disabled per Device setting!', true, 'THOLD');
} elseif ($alert_email != '') {
thold_mail($alert_email, '', '', $subject, $msg, '', '', $notify_list_id, $host, '', 7, 'thold_dhost_mail');
}
$command = read_config_option('thold_device_command');
if ($command != '') {
thold_putenv('THOLD_SUBJECT=' . $subject);
thold_putenv('THOLD_HOSTNAME=' . $host['hostname']);
thold_putenv('THOLD_HOST_ID=' . $host['id']);
thold_putenv('THOLD_DESCRIPTION=' . $host['description']);
thold_putenv('THOLD_SITE=' . $site);
thold_putenv('THOLD_LOCATION=' . $host['location']);
thold_putenv('THOLD_TIME=' . time());
thold_putenv('THOLD_DATE=' . date(CACTI_DATE_TIME_FORMAT));
thold_putenv('THOLD_DATE_RFC822=' . date(DATE_RFC822));
thold_putenv('THOLD_UPTIME=');
thold_putenv('THOLD_DOWNTIME=' . $last_time_duration);
thold_putenv('THOLD_MESSAGE=' . $host['status_last_error']);
thold_putenv('THOLD_DOWNUP=' . 'DOWN');
thold_putenv('THOLD_SNMP_HOSTNAME=' . $host['snmp_sysName']);
thold_putenv('THOLD_SNMP_LOCATION=' . $host['snmp_sysLocation']);
thold_putenv('THOLD_SNMP_CONTACT=' . $host['snmp_sysContact']);
thold_putenv('THOLD_SNMP_SYSTEM=');
thold_putenv('THOLD_LAST_FAIL=' . $host['status_fail_date']);
thold_putenv('THOLD_AVAILABILITY=' . $host['availability']);
thold_putenv('THOLD_TOT_POLL=' . $host['total_polls']);
thold_putenv('THOLD_FAIL_POLL=' . $host['failed_polls']);
thold_putenv('THOLD_CUR_TIME=' . $host['cur_time']);
thold_putenv('THOLD_AVG_TIME=' . $host['avg_time']);
$environment = thold_putenv('THOLD_NOTES=' . $host['notes']);
if (file_exists($command) && is_executable($command)) {
$output = [];
$return = 0;
if ($queue == '') {
exec($command, $output, $return);
cacti_log('Device Down Command for Device[' . $host['id'] . '] Command[' . $command . '] ExitStatus[' . $return . '] Output[' . implode(' ', $output) . ']', false, 'THOLD');
} else {
$data = [
'command' => $command,
'data' => $host,
'environment' => $environment
];
thold_notification_add('thold_dhost_cmd', $data, 'id', $notify_list_id, $host);
}
} else {
cacti_log('WARNING: Device Down Command for Device[' . $host['id'] . '] Command[' . $command . '] Is either Not found or Not executable!', false, 'THOLD');
}
}
}
}
}
// Now lets record all failed hosts
if (read_config_option('remote_storage_method') == 1) {
db_execute_prepared('DELETE
FROM plugin_thold_host_failed
WHERE poller_id = ?',
[$config['poller_id']]);
$hosts = db_fetch_assoc_prepared('SELECT id, status, hostname
FROM host
WHERE disabled = ""
AND poller_id = ?
AND ((status != ? AND status != ?)
OR (status = ? AND status_event_count >= IF(thold_failure_count > 0, thold_failure_count, ?))) ',
[$config['poller_id'], HOST_UP, HOST_DOWN, HOST_DOWN, $ping_failure_count]);
set_config_option('time_last_change_thold_device', time());
} else {
db_execute('TRUNCATE plugin_thold_host_failed');
$hosts = db_fetch_assoc_prepared('SELECT id, status, hostname
FROM host
WHERE disabled = ""
AND ((status != ? AND status != ?)
OR (status = ? AND status_event_count >= IF(thold_failure_count > 0, thold_failure_count, ?))) ',
[HOST_UP, HOST_DOWN, HOST_DOWN, $ping_failure_count]);
set_config_option('time_last_change_thold_device', time());
}
if (cacti_sizeof($hosts)) {
$failed_ids = '';
foreach ($hosts as $host) {
// hosts in recovery status record only if they was in failed status
if (($host['status'] != HOST_RECOVERING) || ($host['status'] == HOST_RECOVERING && (is_array($failed) && array_search($host['id'], array_column($failed, 'host_id'), true) !== false))) {
$failed_ids .= ($failed_ids != '' ? '), (' : '(') . $host['id'];
}
}
$failed_ids .= $failed_ids != '' ? ')' : '';
if ($failed_ids != '') {
db_execute("INSERT INTO plugin_thold_host_failed
(host_id)
VALUES $failed_ids");
set_config_option('time_last_change_thold_device', time());
}
}
return $total_hosts;
}