Files
cacti/cli/update_heartbeat.php
Thomas Vincent 2b6e96b23a style(php): [develop] wrap CLI and poller calls without changing execution (#8303)
* style(php): wrap CLI and poller calls without changing execution

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(ci): review security inventories after poller wrapping

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(boost): compare handoff source contracts by PHP tokens

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* style(php): wrap poller constant strings without changing values

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* style(php): wrap poller concatenations with token-preserving source contracts

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(cli): assert CDEF repair queries after constant folding

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: verify PHP formatting preserves parsed behavior

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-14 00:01:26 -07:00

397 lines
12 KiB
PHP
Executable File

#!/usr/bin/env 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/ |
+-------------------------------------------------------------------------+
*/
require(__DIR__ . '/../include/cli_check.php');
ini_set('max_execution_time', '0');
if (POLLER_ID > 1) {
print 'FATAL: This utility is designed for the main Data Collector only' . PHP_EOL;
exit(1);
}
// process calling arguments
$parms = $_SERVER['argv'];
array_shift($parms);
$debug = false;
$force = false;
$data_template_id = false;
$prev_heartbeat = false;
$new_heartbeat = false;
global $heartbeats;
if (cacti_sizeof($parms)) {
foreach ($parms as $parameter) {
if (str_contains($parameter, '=')) {
[$arg, $value] = explode('=', $parameter, 2);
} else {
$arg = $parameter;
$value = '';
}
switch ($arg) {
case '--new-heartbeat':
$new_heartbeat = $value;
break;
case '--prev-heartbeat':
$prev_heartbeat = $value;
break;
case '--data-template-id':
$data_template_id = $value;
break;
case '--list-data-templates':
$data_templates = db_fetch_assoc('SELECT id, name
FROM data_template
ORDER BY id');
if (cacti_sizeof($data_templates)) {
print 'ID Data Template Name' . PHP_EOL;
print '------ ----------------------' . PHP_EOL;
foreach ($data_templates as $dt) {
printf('%-6s %-25s' . PHP_EOL, $dt['id'], $dt['name']);
}
print PHP_EOL;
} else {
print 'WARNING: No Data Templates found!' . PHP_EOL;
}
exit(0);
case '--list-heartbeats':
if (cacti_sizeof($heartbeats)) {
print 'Heartbeat Heartbeat Name' . PHP_EOL;
print '--------- --------------' . PHP_EOL;
foreach ($heartbeats as $index => $hb) {
printf('%-12s%-17s' . PHP_EOL, $index, $hb);
}
print PHP_EOL;
} else {
print 'WARNING: No Heartbeats found!' . PHP_EOL;
}
exit(0);
case '--list-profiles':
$dspheartbeats = db_fetch_assoc('SELECT name, heartbeat
FROM data_source_profiles
ORDER BY heartbeat');
if (cacti_sizeof($dspheartbeats)) {
print 'Heartbeat Data Source Profile' . PHP_EOL;
print '--------- ----------------------' . PHP_EOL;
foreach ($dspheartbeats as $hb) {
printf('%-12s%-25s' . PHP_EOL, $hb['heartbeat'], $hb['name']);
}
print PHP_EOL;
} else {
print 'WARNING: No Data Source Profiles found!' . PHP_EOL;
}
exit(0);
case '--debug':
$debug = true;
break;
case '--force':
$force = true;
break;
case '--version':
case '-V':
case '-v':
display_version();
exit(0);
case '--help':
case '-H':
case '-h':
display_help();
exit(0);
default:
print 'ERROR: Invalid Parameter ' . $parameter . PHP_EOL . PHP_EOL;
display_help();
exit(1);
}
}
} else {
print 'ERROR: You must supply input parameters' . PHP_EOL . PHP_EOL;
display_help();
exit(1);
}
$poller_interval = read_config_option('poller_interval');
if ($poller_interval * 2.0 > $new_heartbeat) {
printf('ERROR: Your new heartbeat must be greater or equal than %.0f' . PHP_EOL, $poller_interval * 2);
exit(1);
}
$sql_where = '';
$sql_params = [];
if ($data_template_id !== false && $data_template_id > 0) {
$sql_params[] = $data_template_id;
$sql_where = ' AND dtd.data_template_id = ?';
} elseif ($data_template_id !== false && ($data_template_id <= 0 || !is_numeric($data_template_id))) {
printf('ERROR: Your Data Template ID \'%s\' is invalid' . PHP_EOL, $data_template_id);
exit(1);
}
if ($new_heartbeat !== false && (!is_numeric($new_heartbeat) || $new_heartbeat < -1)) {
printf('ERROR: Your New Heartbeat \'%s\' must be greater or equal to zero.' . PHP_EOL, $new_heartbeat);
exit(1);
}
if (!array_key_exists($new_heartbeat, $heartbeats)) {
printf(('ERROR: Your New Heartbeat \'%s\' is not a supported Heartbeat. Use ' .
'--list-heartbeats to see the list.') . PHP_EOL,
$new_heartbeat);
exit(1);
}
if ($prev_heartbeat !== false && (!is_numeric($prev_heartbeat) || $prev_heartbeat < -1)) {
printf('ERROR: Your Previous Heartbeat \'%s\' must be greater or equal to zero' . PHP_EOL, $prev_heartbeat);
} elseif ($prev_heartbeat !== false) {
$sql_where .= ' AND dtr.rrd_heartbeat = ?';
$sql_params[] = $prev_heartbeat;
}
$sql_params1 = array_merge([CACTI_PATH_RRA], $sql_params);
$rrdfiles = db_fetch_assoc_prepared("SELECT dtr.local_data_id, dtd.name_cache, dt.name,
REPLACE(dtd.data_source_path, '<path_rra>', ?) AS rrd,
dtr.rrd_heartbeat, GROUP_CONCAT(DISTINCT dtr.data_source_name) AS data_sources
FROM data_template_data AS dtd
INNER JOIN data_template AS dt
ON dtd.data_template_id = dt.id
INNER JOIN data_template_rrd AS dtr
ON dtd.local_data_id = dtr.local_data_id
WHERE dtd.local_data_id > 0
$sql_where
GROUP BY dtd.local_data_id",
$sql_params1);
$total_heartbeats = array_rekey($rrdfiles, 'rrd_heartbeat', 'rrd_heartbeat');
$profile_ids = db_fetch_assoc_prepared("SELECT DISTINCT dsp.id, dsp.heartbeat, dsp.name
FROM data_template_data AS dtd
INNER JOIN data_template_rrd AS dtr
ON dtd.local_data_id = dtr.local_data_id
INNER JOIN data_source_profiles AS dsp
ON dsp.id = dtd.data_source_profile_id
WHERE dtd.local_data_id > 0
$sql_where",
$sql_params);
if (cacti_sizeof($profile_ids) > 1) {
printf('There are %s Data Source Proiles Impacted by this change.' . PHP_EOL, cacti_sizeof($profile_ids));
} elseif (cacti_sizeof($profile_ids) == 1) {
printf('There is %s Data Source Proile Impacted by this change.' . PHP_EOL, cacti_sizeof($profile_ids));
} else {
printf('ERROR: Not RRDfiles found that match your --prev-heartbeat or --data-template-id setting' . PHP_EOL);
exit(1);
}
printf('Current Data Source Profile Heartbeats found are: %s' . PHP_EOL, implode(', ', $total_heartbeats));
printf('There will be %s RRDfiles updated as a result of this command.' . PHP_EOL, cacti_sizeof($rrdfiles));
if (!$force) {
$exit = false;
foreach ($profile_ids as $pid) {
if ($pid['heartbeat'] != $new_heartbeat) {
printf('ERROR: Data Source Profile \'%s\' has a heartbeat of %s which does' . PHP_EOL,
$pid['name'],
$pid['heartbeat']);
printf(' not match the new heartbeat of %s. Use the --force option to update' . PHP_EOL, $new_heartbeat);
printf(' the Data Source Profile\'s heartbeat as well as the Data Sources.' . PHP_EOL . PHP_EOL);
$exit = true;
}
}
if ($exit) {
exit(1);
}
} else {
printf('This is a forced run, impacted Data Source Profiles will have their Heartbeats updated as well' . PHP_EOL);
}
$i = 0;
// keep the historical binary name when the setting has never been filled in
$rrdtool_bin = read_config_option('path_rrdtool');
if ($rrdtool_bin == '') {
$rrdtool_bin = 'rrdtool';
}
if (cacti_sizeof($rrdfiles)) {
foreach ($rrdfiles as $f) {
if (file_exists($f['rrd'])) {
$command = cacti_escapeshellcmd($rrdtool_bin) . ' tune ' . cacti_escapeshellarg($f['rrd']);
$data_sources = explode(',', $f['data_sources']);
foreach ($data_sources as $ds) {
$command .= ' --heartbeat ' . cacti_escapeshellarg($ds . ':' . $new_heartbeat);
}
$output = [];
$return_code = 0;
debug(sprintf('Updating Heartbeat for Data Source:%s, Data Template:%s, RRD:%s from 600 to 900' . PHP_EOL,
$f['name_cache'],
$f['name'],
$f['rrd']));
debug(sprintf("The RRDtool command is '$command'" . PHP_EOL));
$result = exec($command, $output, $return_code);
if ($return_code != 0) {
printf('Warning Error Occurred: ' . implode(', ', $output) . PHP_EOL);
} else {
db_execute_prepared('UPDATE data_template_rrd
SET rrd_heartbeat = ?
WHERE local_data_id = ?',
[$new_heartbeat, $f['local_data_id']]);
}
} else {
printf('WARNING: RRDfile \'%s\' does not exist!' . PHP_EOL, $f['rrd']);
}
$i++;
if ($i % 100 == 0) {
printf('Processed %s RRDfiles' . PHP_EOL, $i);
}
}
printf('Processed a Total of %s RRDfiles' . PHP_EOL, $i);
if ($data_template_id > 0) {
db_execute_prepared('UPDATE data_template_rrd
SET rrd_heartbeat = ?
WHERE local_data_id = 0
AND data_template_id = ?',
[$new_heartbeat, $data_template_id]);
}
if ($force) {
foreach ($profile_ids as $pid) {
db_execute_prepared('UPDATE data_source_profiles
SET heartbeat = ?
WHERE id = ?',
[$new_heartbeat, $pid['id']]);
}
if ($data_template_id > 0) {
db_execute_prepared('UPDATE data_template_rrd
SET rrd_heartbeat = ?
WHERE local_data_id = 0
AND data_template_id = ?',
[$new_heartbeat, $data_template_id]);
} else {
db_execute_prepared('UPDATE data_template_rrd
SET rrd_heartbeat = ?
WHERE local_data_id = 0',
[$new_heartbeat]);
}
}
}
function debug(string $message) : void {
global $debug;
if ($debug) {
print 'DEBUG: ' . trim($message) . PHP_EOL;
}
}
/**
* display_version - displays version information
*
* @return void
*/
function display_version() : void {
$version = get_cacti_cli_version();
print "Cacti Update RRDfile Heartbeat Utility, Version $version, " . COPYRIGHT_YEARS . PHP_EOL;
}
/**
* display_help - displays the usage of the function
*
* @return void
*/
function display_help() : void {
display_version();
print PHP_EOL;
print ('usage: update_heartbeat.php --new-heartbeat=N [--data-template-id=id] ' .
'[--prev-heartbeat=N] [--force] [--debug|-d]') . PHP_EOL . PHP_EOL;
print 'A utility to update RRDfile heartbeats and the Cacti database to match.' . PHP_EOL . PHP_EOL;
print 'Required:' . PHP_EOL;
print (' --new-heartbeat=N - A Heartbeat in seconds. It must align ' .
'with available Heartbeats in Cacti') . PHP_EOL;
print (' Currently Heartbeats include from ' .
'20-172800 seconds. The value must also') . PHP_EOL;
print ' be at least two times the current Cacti poller interval.' . PHP_EOL;
print 'Optional:' . PHP_EOL;
print (' --data-template-id=N - Only update Cacti Data Source Heartbeats ' .
'that are associated with a Data Template id.') . PHP_EOL;
print (' --prev-heartbeat=N - Only update Cacti Data Sources that ' .
'currently have the Heartbeat specified.') . PHP_EOL;
print (' --force - If the heartbeat selected does not match ' .
'the Data Source Profile, update the') . PHP_EOL;
print (' Data Source Profile to match the command. ' .
' Otherwise, the script will exit.') . PHP_EOL;
print ' --debug - Display verbose output during execution' . PHP_EOL . PHP_EOL;
print 'List Options:' . PHP_EOL;
print ' --list-data-templates - List all Data Templates' . PHP_EOL;
print ' --list-heartbeats - List all supported Heartbeats' . PHP_EOL;
print ' --list-profiles - List all Data Source Profiles and their Heartbeats' . PHP_EOL . PHP_EOL;
}