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>
This commit is contained in:
TheWitnessandCopilot authored and GitHub committed 2026-10-15 13:51:47 -04:00
1 parent dd2abc92cd
commit 2c7593c990
4 files changed
+132 -3

No files matched your search

+1 -1
View File
@@ -3423,7 +3423,7 @@ function thold_check_threshold(&$thold_data) {
}
}
if (api_plugin_is_enabled('maint') && plugin_maint_check_cacti_host($thold_data['host_id'])) {
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;
+2 -2
View File
@@ -637,7 +637,7 @@ function thold_update_host_status() {
$msg = thold_str_replace('<NOTES>', $host['notes'], $msg);
$msg = thold_str_replace("\n", '<br>', $msg);
if (api_plugin_is_enabled('maint') && plugin_maint_check_cacti_host($host['id'])) {
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']) {
@@ -764,7 +764,7 @@ function thold_update_host_status() {
$alert_email = read_config_option('alert_email');
if (api_plugin_is_enabled('maint') && plugin_maint_check_cacti_host($host['id'])) {
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) {
+123
View File
@@ -0,0 +1,123 @@
<?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. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
/**
* thold_update_host_status() consults the maint plugin for both the
* recovering-device and down-device notification loops. The call is now
* guarded by function_exists('plugin_maint_check_cacti_host') so a maint
* plugin that is enabled but whose functions file could not be included no
* longer fatals with an undefined-function error. These tests drive both
* maintenance-gated branches so that guard stays in place.
*/
final class MaintCheckGuardTest extends TestCase {
/**
* @return void
*/
public static function setUpBeforeClass(): void {
self::loadPluginSource('includes/functions.php');
self::loadPluginSource('includes/polling.php');
self::loadPluginSourceAlways('includes/arrays.php');
self::loadPluginConstants();
}
/**
* @return string
*/
private function maintIncludesRoot() {
return dirname(__DIR__) . '/Fixtures/cacti-root-maint-includes';
}
/**
* Run thold_update_host_status() against the newer maint layout. A device
* inside a maintenance window is logged and the loop moves on without the
* downstream notification work, so the call returns on its own. Expected
* engine warnings are suppressed, but no Throwable is swallowed, so an
* unexpected error (e.g. a removed guard fataling on an undefined maint
* function) fails the test.
*
* @return void
*/
private function runHostStatus() {
CactiStubs::willAlwaysReturn('api_plugin_is_enabled', true);
CactiStubs::willAlwaysReturn('plugin_maint_check_cacti_host', true);
CactiStubs::$configOptions['alert_deadnotify'] = 'on';
$GLOBALS['config']['base_path'] = $this->maintIncludesRoot();
set_error_handler(static fn (): bool => true);
ob_start();
try {
thold_update_host_status();
} finally {
ob_end_clean();
restore_error_handler();
}
}
/**
* A recovered device inside a maintenance window is logged, not alerted.
*
* @return void
*/
public function testRecoveringDeviceInMaintenanceIsLoggedNotAlerted(): void {
CactiStubs::willReturnFor('db_fetch_assoc', 'plugin_thold_host_failed', [['host_id' => 42]]);
CactiStubs::willReturnFor('db_fetch_row_prepared', 'FROM host WHERE id', [
'id' => 42,
'hostname' => 'dev42',
'description' => 'Device 42',
'status' => HOST_UP,
'snmp_community' => '',
'snmp_username' => '',
'snmp_version' => 0,
'site_id' => 0,
'location' => '',
'notes' => '',
'status_fail_date' => '0000-00-00 00:00:00',
'availability' => 100,
'total_polls' => 1,
'failed_polls' => 0,
'cur_time' => 0,
'avg_time' => 0,
'thold_send_email' => '0',
]);
$this->runHostStatus();
$this->assertContains(
'WARNING: Device[42] Hostname[dev42] is recovering! Only logging, maint device',
CactiStubs::$log
);
}
/**
* A down device inside a maintenance window is logged, not alerted.
*
* @return void
*/
public function testDownDeviceInMaintenanceIsLoggedNotAlerted(): void {
CactiStubs::willReturnFor('db_fetch_assoc_prepared', 'availability_method', [[
'id' => 77,
'hostname' => 'dev77',
]]);
$this->runHostStatus();
$this->assertContains(
'WARNING: Device[77] Hostname[dev77] is DOWN. Only logging, maint device',
CactiStubs::$log
);
}
}
+6
View File
@@ -669,6 +669,12 @@ if (!function_exists('number_format_i18n')) {
}
}
if (!function_exists('html_split_string')) {
function html_split_string($string, $length = 76, $forgiving = 10) {
return $string;
}
}
if (!defined('FILTER_VALIDATE_IS_REGEX')) {
define('FILTER_VALIDATE_IS_REGEX', 99999);
}