mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-24 12:07:31 +09:00
* 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>
1109 lines
35 KiB
PHP
Executable File
1109 lines
35 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/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
// take your time
|
|
ini_set('max_execution_time', 0);
|
|
|
|
// change working directory
|
|
chdir(__DIR__ . '/../');
|
|
|
|
// get access to the Cacti database
|
|
require_once('./include/cli_check.php');
|
|
|
|
// process calling arguments
|
|
$parms = $_SERVER['argv'];
|
|
array_shift($parms);
|
|
|
|
if (cacti_sizeof($parms)) {
|
|
$data_template_id = false;
|
|
$displayDataTemplates = false;
|
|
$debug_mode = false;
|
|
$dry_run_mode = false;
|
|
$backup_folder = false;
|
|
$tmp_backup_folder = false;
|
|
|
|
foreach ($parms as $parameter) {
|
|
if (str_contains($parameter, '=')) {
|
|
[$arg, $value] = explode('=', $parameter, 2);
|
|
} else {
|
|
$arg = $parameter;
|
|
$value = '';
|
|
}
|
|
|
|
switch ($arg) {
|
|
case '-d':
|
|
$debug_mode = true;
|
|
|
|
break;
|
|
case '--debug':
|
|
$debug_mode = true;
|
|
|
|
break;
|
|
case '--data-template-id':
|
|
$data_template_id = trim($value);
|
|
|
|
if (!is_numeric($data_template_id)) {
|
|
print 'ERROR: You must supply a valid data template id to run this script!' . PHP_EOL;
|
|
|
|
exit(1);
|
|
} else {
|
|
$data_template_id__valid = db_fetch_cell('SELECT id FROM data_template WHERE id =' . $data_template_id);
|
|
|
|
if ($data_template_id__valid === null) {
|
|
print 'ERROR: You must supply a valid data template id to run this script!' . PHP_EOL;
|
|
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
break;
|
|
case '--backup':
|
|
$backup_folder = trim($value);
|
|
|
|
if (!$backup_folder) {
|
|
print 'ERROR: You must supply a valid backup folder to run this script!' . PHP_EOL;
|
|
|
|
exit(1);
|
|
}
|
|
|
|
if (!file_exists($backup_folder)) {
|
|
if (mkdir($backup_folder, 0777, true) === false) {
|
|
print "ERROR: Cannot create backup folder: \"$backup_folder\"" . PHP_EOL;
|
|
|
|
exit(1);
|
|
}
|
|
} else {
|
|
if (is_dir($backup_folder) === false) {
|
|
print "ERROR: Given backup folder: \"$backup_folder\" is not a directory" . PHP_EOL;
|
|
|
|
exit(1);
|
|
} else {
|
|
if (!is_writable($backup_folder)) {
|
|
print "ERROR: No write permission to backup folder: \"$backup_folder\" given." . PHP_EOL;
|
|
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
$tmp_backup_folder = $backup_folder . '/' . date('d-m-Y_G-i-s') . '/';
|
|
|
|
break;
|
|
case '--dry-run':
|
|
$dry_run_mode = true;
|
|
|
|
break;
|
|
case '--list-data-templates':
|
|
$displayDataTemplates = true;
|
|
|
|
break;
|
|
case '--version':
|
|
case '-v':
|
|
case '-V':
|
|
display_version();
|
|
|
|
exit(0);
|
|
case '-h':
|
|
case '-H':
|
|
case '--help':
|
|
display_help();
|
|
|
|
exit(0);
|
|
|
|
default:
|
|
print "ERROR: Invalid Argument: ($arg)" . PHP_EOL . PHP_EOL;
|
|
display_help();
|
|
|
|
exit(1);
|
|
}
|
|
}
|
|
} else {
|
|
display_help();
|
|
|
|
exit(0);
|
|
}
|
|
|
|
display_version();
|
|
print 'THIS SCRIPT IS CURRENTLY RELYING ON TABLES THAT DO NOT EXIST' . PHP_EOL . PHP_EOL;
|
|
|
|
// presets
|
|
$logging = 1;
|
|
$total_files = 0;
|
|
$total_files_skipped = 0;
|
|
$total_errors = 0;
|
|
$total_outdated = 0;
|
|
$total_mismatches = 0;
|
|
$total_skipped = 0;
|
|
|
|
if ($displayDataTemplates) {
|
|
$data_templates = db_fetch_assoc('SELECT id, name
|
|
FROM data_template
|
|
ORDER BY id, name');
|
|
|
|
print 'Known Data Templates: (id, name)' . PHP_EOL;
|
|
|
|
if (cacti_sizeof($data_templates)) {
|
|
foreach ($data_templates as $data_template) {
|
|
print $data_template['id'] . "\t" . $data_template['name'] . PHP_EOL;
|
|
}
|
|
} else {
|
|
print 'ERROR: No data templates found.' . PHP_EOL;
|
|
}
|
|
print PHP_EOL;
|
|
|
|
exit(0);
|
|
}
|
|
|
|
if (!$data_template_id) {
|
|
print 'ERROR: You must supply a valid data template id to run this script!' . PHP_EOL;
|
|
|
|
exit(1);
|
|
}
|
|
|
|
if (!$backup_folder) {
|
|
print 'ERROR: You must define a backup folder!' . PHP_EOL;
|
|
|
|
exit(1);
|
|
} else {
|
|
$tmp_xml_file = $tmp_backup_folder . 'tmp_rrdresize.xml';
|
|
$tmp_rrd_file = $tmp_backup_folder . 'tmp_rrdfile.rrd';
|
|
}
|
|
|
|
// check status of Boost
|
|
$boost_enabled = (function_exists('boost_process_poller_output') &&
|
|
db_fetch_cell("SELECT 1 FROM `settings`
|
|
WHERE name = 'boost_rrd_update_enable'
|
|
AND value = 'on'")) ? true : false;
|
|
|
|
$boost_server_enabled = (db_fetch_cell("SELECT 1 FROM `settings`
|
|
WHERE name = 'boost_server_enable'
|
|
AND value = 'on'")) ? true : false;
|
|
|
|
// setup paths
|
|
$path_rrdtool = read_config_option('path_rrdtool');
|
|
$path_rra = getcwd() . DIRECTORY_SEPARATOR . 'rra';
|
|
|
|
// setup RRDtool Pipes
|
|
$rrdtool_process_pipes = rrdtool_pipe_init($path_rrdtool);
|
|
$rrdtool_process = $rrdtool_process_pipes[0];
|
|
$rrdtool_pipes = $rrdtool_process_pipes[1];
|
|
|
|
// scan the system to get an idea of how files and folders we have
|
|
$scanned_directory = dirToArrayOne($path_rra);
|
|
/*
|
|
$scanned_directory = array(
|
|
'files' => 0,
|
|
'folders' => 1,
|
|
'content' => Array
|
|
(
|
|
2446 => Array
|
|
(
|
|
'files' => 1,
|
|
'folders' => 0,
|
|
'content' => Array
|
|
(
|
|
0 => '42194.rrd',
|
|
)
|
|
|
|
)
|
|
)
|
|
);
|
|
*/
|
|
|
|
$known_cfs = [1 => 'AVERAGE', 2 => 'MIN', 3 => 'MAX', 4 => 'LAST'];
|
|
$known_ds_types = [1 => 'GAUGE', 2 => 'COUNTER', 3 => 'DERIVE', 4 => 'ABSOLUTE'];
|
|
|
|
if ($scanned_directory['folders']) {
|
|
// create backup folder
|
|
if (!mkdir((string) $tmp_backup_folder)) {
|
|
print 'ERROR: Unable to create a backup folder!' . PHP_EOL;
|
|
|
|
exit(1);
|
|
}
|
|
|
|
// create log file
|
|
$log_handle = fopen($tmp_backup_folder . 'tmp_rrdresize.log', 'w');
|
|
|
|
f_notify('Boost Plugin Status', ($boost_enabled ? 'enabled' : 'disabled'));
|
|
f_log('Boost Plugin Status ' . ($boost_enabled ? 'enabled' : 'disabled'));
|
|
f_notify('Boost Server Status', ($boost_server_enabled ? 'enabled' : 'disabled'));
|
|
f_log('Boost Server Status ' . ($boost_server_enabled ? 'enabled' : 'disabled'));
|
|
f_log('Total number of RRD files found: ' . $total_files);
|
|
|
|
// generate a list of hosts using this data template id
|
|
$db_hosts = db_fetch_assoc_prepared('SELECT DISTINCT host_id
|
|
FROM data_local
|
|
WHERE data_template_id = ?',
|
|
[$data_template_id]
|
|
);
|
|
|
|
$dt_hosts = [];
|
|
|
|
if (cacti_sizeof($db_hosts) > 0) {
|
|
foreach ($db_hosts as $key => $value) {
|
|
$dt_hosts[] = $value['host_id'];
|
|
}
|
|
} else {
|
|
print 'No hosts found.' . PHP_EOL;
|
|
|
|
exit(1);
|
|
}
|
|
|
|
$db_data_sources = db_fetch_assoc_prepared('SELECT DISTINCT local_data_id
|
|
FROM data_template_data
|
|
WHERE data_template_id = ?
|
|
AND data_source_path IS NOT NULL',
|
|
[$data_template_id]
|
|
);
|
|
|
|
$dt_data_sources = [];
|
|
|
|
if (cacti_sizeof($db_data_sources) > 0) {
|
|
foreach ($db_data_sources as $key => $value) {
|
|
$dt_data_sources[] = $value['local_data_id'];
|
|
}
|
|
} else {
|
|
print 'No data sources found.' . PHP_EOL;
|
|
|
|
exit(1);
|
|
}
|
|
|
|
$subfolders = $scanned_directory['content'];
|
|
|
|
$counter = 0;
|
|
|
|
foreach ($subfolders as $host_id => $host_data) {
|
|
if (!in_array($host_id, $dt_hosts, true)) {
|
|
continue;
|
|
}
|
|
|
|
if ($host_data['files']) {
|
|
$host_files = $host_data['content'];
|
|
|
|
foreach ($host_files as $host_file) {
|
|
$local_data_id = intval(substr($host_file, 0, -4));
|
|
|
|
if (!in_array($local_data_id, $dt_data_sources, true)) {
|
|
continue;
|
|
}
|
|
|
|
// Data Template Data
|
|
$file = $path_rra . DIRECTORY_SEPARATOR . $host_id . DIRECTORY_SEPARATOR . $host_file;
|
|
|
|
if (!$local_data_id) {
|
|
f_log('[ERROR] Unable to detect local data id: ' . $file);
|
|
$total_errors++;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!is_readable($file)) {
|
|
f_log('[ERROR] : Read permissions required for file: ' . $file);
|
|
$total_errors++;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!is_resource_writable($file)) {
|
|
f_log('[ERROR] : Write permissions required for file: ' . $file);
|
|
$total_errors++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$search_pattern = '<path_rra>/' . $host_id . '/' . $host_file;
|
|
|
|
$local_data = db_fetch_row_prepared('SELECT *
|
|
FROM data_template_data
|
|
WHERE data_source_path = ?',
|
|
[$search_pattern]);
|
|
|
|
if ($local_data) {
|
|
if ($local_data['data_template_id'] != $data_template_id) {
|
|
// this file has not to be update due to template mismatches
|
|
f_log('[SKIPPED] Template mismatch: ' . $file);
|
|
|
|
continue;
|
|
}
|
|
|
|
f_notify('File', $file);
|
|
f_notify('Local Data ID', $local_data_id);
|
|
f_notify('Cacti Settings ');
|
|
|
|
// grep all data template settings
|
|
$data_template_ds = db_fetch_assoc_prepared("SELECT id, rrd_maximum, rrd_minimum, rrd_heartbeat,
|
|
data_source_type_id, data_source_name
|
|
FROM data_template_rrd
|
|
WHERE hash != ''
|
|
AND data_template_id = ? ORDER BY id",
|
|
[$local_data['data_template_id']]
|
|
);
|
|
|
|
$data_template_ds_counter = cacti_sizeof($data_template_ds);
|
|
$data_template_data_settings = db_fetch_assoc_prepared('SELECT *
|
|
FROM data_template_data_rra
|
|
LEFT JOIN rra
|
|
ON rra.id = data_template_data_rra.rra_id
|
|
RIGHT JOIN rra_cf
|
|
ON rra_cf.rra_id = rra.id
|
|
WHERE data_template_data_rra.data_template_data_id = ?',
|
|
[$local_data['id']]
|
|
);
|
|
|
|
f_notify(false, "\033[0;32m[COMPLETED]\033[0m");
|
|
} else {
|
|
f_log('[OUTDATED] Unable to detect referenced local data id: ' . $file);
|
|
|
|
continue;
|
|
}
|
|
|
|
// BOOST update process
|
|
f_notify('Boost update');
|
|
|
|
if ($boost_enabled && !$dry_run_mode) {
|
|
$boost_update = true; // placeholder
|
|
// ----- run on demand update if Boost is enabled and cached data is part of the report period -----
|
|
$output = boost_process_poller_output($local_data_id);
|
|
f_notify(false, "\033[0;32m[COMPLETED]\033[0m");
|
|
} else {
|
|
f_notify(false, "\033[0;36m[SKIPPED]\033[0m");
|
|
}
|
|
|
|
f_notify('RRDtool info');
|
|
$output = rrdtool_pipe_execute(' info ' . $file . "\r\n", $rrdtool_pipes);
|
|
|
|
if (str_contains($output, 'ERROR')) {
|
|
f_notify(false, "\033[0;31m[FAILED]\033[0m");
|
|
f_log('[ERROR] Unable to fetch RRDtool Info: ' . $file);
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($output == '') {
|
|
f_notify(false, "\033[0;31m[FAILED]\033[0m");
|
|
f_log('[ERROR] Unable to fetch RRDtool Info: ' . $file);
|
|
|
|
continue;
|
|
} else {
|
|
$rrd_info = rrdtool_parse_info($output);
|
|
|
|
if ($rrd_info) {
|
|
f_notify(false, "\033[0;32m[COMPLETED]\033[0m");
|
|
} else {
|
|
f_notify(false, "\033[0;31m[FAILED]\033[0m");
|
|
f_log('[ERROR] Unable to parse RRDtool Info: ' . $file);
|
|
|
|
continue;
|
|
}
|
|
|
|
// Analyze Data Structure
|
|
f_notify('DS Structure');
|
|
$ds_mismatch = false;
|
|
$tmp_ds = $rrd_info['ds'];
|
|
$ds_mismatch_fixable = false;
|
|
$ds_error = false;
|
|
|
|
foreach ($data_template_ds as $data_template__ds_settings) {
|
|
$found = false;
|
|
$defined_ds_name = $data_template__ds_settings['data_source_name'];
|
|
$defined_ds_type = $known_ds_types[$data_template__ds_settings['data_source_type_id']];
|
|
$defined_minimum = is_numeric($data_template__ds_settings['rrd_minimum']) ? $data_template__ds_settings['rrd_minimum'] : 'NaN';
|
|
$defined_maximum = is_numeric($data_template__ds_settings['rrd_maximum']) ? $data_template__ds_settings['rrd_maximum'] : 'NaN';
|
|
$defined_min_heartbeat = $data_template__ds_settings['rrd_heartbeat'];
|
|
|
|
if (isset($tmp_ds[$defined_ds_name])) {
|
|
$tmp_ds_rrd = $tmp_ds[$defined_ds_name];
|
|
|
|
if ($tmp_ds_rrd['type'] == $defined_ds_type &&
|
|
$tmp_ds_rrd['minimal_heartbeat'] == $defined_min_heartbeat &&
|
|
$tmp_ds_rrd['min'] == $defined_minimum &&
|
|
$tmp_ds_rrd['max'] == $defined_maximum) {
|
|
$found = true;
|
|
} else {
|
|
$ds_mismatch = true;
|
|
$ds_error = 'definition';
|
|
$ds_mismatch_fixable = ($tmp_ds_rrd['type'] == $defined_ds_type) ? true : false;
|
|
|
|
if ($tmp_ds_rrd['minimal_heartbeat'] != $defined_min_heartbeat) {
|
|
$rrd_info['ds'][$defined_ds_name]['minimal_heartbeat'] = $defined_min_heartbeat;
|
|
f_log('[NOTICE] Data Source \'' . $defined_ds_name . '\' heartbeat mismatch: ' . $file);
|
|
}
|
|
|
|
if ($tmp_ds_rrd['min'] != $defined_minimum) {
|
|
$rrd_info['ds'][$defined_ds_name]['min'] = $defined_minimum;
|
|
f_log('[NOTICE] Data Source \'' . $defined_ds_name . '\' minimum mismatch: ' . $file);
|
|
}
|
|
|
|
if ($tmp_ds_rrd['max'] != $defined_maximum) {
|
|
$rrd_info['ds'][$defined_ds_name]['max'] = $defined_maximum;
|
|
f_log('[NOTICE] Data Source \'' . $defined_ds_name . '\' maximum mismatch: ' . $file);
|
|
}
|
|
}
|
|
unset($tmp_ds[$defined_ds_name]);
|
|
} else {
|
|
$ds_mismatch = true;
|
|
$ds_error = 'missing';
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
$ds_superfluous_ids = [];
|
|
|
|
if ($ds_mismatch == true) {
|
|
if ($ds_error == 'missing') {
|
|
f_notify(false, "\033[0;31m[COUNT - MISSING DS]\033[0m");
|
|
f_log('[ERROR] Data source missing: ' . $file);
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($ds_error == 'definition') {
|
|
if (!$ds_mismatch_fixable) {
|
|
f_notify(false, "\033[0;31m[UNFIXABLE]\033[0m");
|
|
f_log('[ERROR] Unable to fix misconfigured data source: ' . $file);
|
|
|
|
continue;
|
|
} else {
|
|
f_notify(false, "\033[0;35m[FIXABLE]\033[0m");
|
|
}
|
|
}
|
|
} else {
|
|
f_notify(false, "\033[0;32m[MATCH]\033[0m");
|
|
}
|
|
|
|
if (!empty($tmp_ds)) {
|
|
// Additional ds found ! :|
|
|
$ds_names = array_keys($rrd_info['ds']);
|
|
|
|
foreach ($tmp_ds as $tmp_ds_name => $tmp_ds_settings) {
|
|
$ds_superfluous_ids += array_keys($ds_names, $tmp_ds_name, true);
|
|
}
|
|
|
|
f_notify('DS COUNT');
|
|
f_notify(false, "\033[0;35m[MISMATCH]\033[0m");
|
|
f_log('[NOTICE] Superfluous data sources \'' . implode(',', $ds_superfluous_ids) . '\' found: ' . $file);
|
|
} else {
|
|
f_notify('DS COUNT');
|
|
f_notify(false, "\033[0;32m[MATCH]\033[0m");
|
|
}
|
|
|
|
f_notify('RRA Structure');
|
|
$rra_mismatch = false;
|
|
$tmp_rras = $rrd_info['rra'];
|
|
|
|
foreach ($data_template_data_settings as $data_template__rra_settings) {
|
|
// rra exists - let us compare the details
|
|
$found = false;
|
|
$defined_cf = $known_cfs[$data_template__rra_settings['consolidation_function_id']];
|
|
$defined_rows = $data_template__rra_settings['rows'];
|
|
$defined_xff = $data_template__rra_settings['x_files_factor'];
|
|
$defined_ppr = $data_template__rra_settings['steps'];
|
|
|
|
foreach ($tmp_rras as $tmp_rra) {
|
|
if ($tmp_rra['cf'] == $defined_cf &&
|
|
$tmp_rra['rows'] == $defined_rows &&
|
|
$tmp_rra['xff'] == $defined_xff &&
|
|
$tmp_rra['pdp_per_row'] == $defined_ppr) {
|
|
$found = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$found) {
|
|
$rra_mismatch = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($rra_mismatch == false) {
|
|
f_notify(false, "\033[0;32m[MATCH]\033[0m");
|
|
} else {
|
|
f_notify(false, "\033[0;35m[MISMATCH]\033[0m");
|
|
}
|
|
}
|
|
|
|
if ($rra_mismatch == false & $ds_mismatch == false) {
|
|
f_notify('Update', "\033[0;35m[NOT NECESSARY]\033[0m");
|
|
|
|
continue;
|
|
}
|
|
|
|
f_notify('RRDtool dump');
|
|
$file_dump = rrdtool_pipe_execute(' dump ' . $file . "\r\n", $rrdtool_pipes);
|
|
|
|
if (str_contains($file_dump, 'ERROR')) {
|
|
f_notify(false, "\033[0;31m[FAILED]\033[0m");
|
|
|
|
// ## log_missing
|
|
continue;
|
|
} else {
|
|
$rrd_data = json_decode((string) json_encode(simplexml_load_string($file_dump)), true);
|
|
|
|
if ($rrd_data === false) {
|
|
f_notify(false, "\033[0;31m[FAILED]\033[0m");
|
|
|
|
// ## log_missing
|
|
continue;
|
|
} else {
|
|
f_notify(false, "\033[0;32m[COMPLETED]\033[0m");
|
|
}
|
|
}
|
|
|
|
// Re-configuration process starts
|
|
f_notify('Rebuilding RRAs');
|
|
|
|
$rra_timespans = [];
|
|
$tmp_step = $rrd_info['step'];
|
|
|
|
foreach ($tmp_rras as $rra_index => $rra_settings) {
|
|
$rra_cf = $rra_settings['cf'];
|
|
$step = $rra_settings['pdp_per_row'] * $tmp_step;
|
|
|
|
$end = $rrd_info['last_update'] - $rrd_info['last_update'] % ($tmp_step * $rra_settings['pdp_per_row']);
|
|
$start = $end - ($rra_settings['rows'] - 1) * $step;
|
|
|
|
$rra_timespans[$rra_settings['cf']][$rra_index] = [
|
|
'step' => $step,
|
|
'start' => $start,
|
|
'end' => $end
|
|
];
|
|
}
|
|
|
|
$rrd_new_header = '<?xml version="1.0" encoding="utf-8"?>' . PHP_EOL .
|
|
'<!DOCTYPE rrd SYSTEM "http://oss.oetiker.ch/rrdtool/rrdtool.dtd">' . PHP_EOL .
|
|
'<rrd>' . PHP_EOL .
|
|
"\t<version>0003</version>" . PHP_EOL .
|
|
"\t<step>300</step>" . PHP_EOL .
|
|
"\t<lastupdate>" . $rrd_info['last_update'] . '</lastupdate>' . PHP_EOL;
|
|
|
|
$rrd_new_ds_definition = '';
|
|
$i = 0;
|
|
|
|
foreach ($rrd_info['ds'] as $rrd_info_ds_name => $rrd_info_ds_settings) {
|
|
if (!in_array($i, $ds_superfluous_ids, true)) {
|
|
$rrd_new_ds_definition__ds_definition_template = "\t<ds>" . PHP_EOL .
|
|
"\t\t<name> " . $rrd_info_ds_name . ' </name>' . PHP_EOL .
|
|
"\t\t<type> " . $rrd_info_ds_settings['type'] . ' </type>' . PHP_EOL .
|
|
"\t\t<minimal_heartbeat>" . $rrd_info_ds_settings['minimal_heartbeat'] . '</minimal_heartbeat>' . PHP_EOL .
|
|
"\t\t<min>" . $rrd_info_ds_settings['min'] . '</min>' . PHP_EOL .
|
|
"\t\t<max>" . $rrd_info_ds_settings['max'] . '</max>' . PHP_EOL .
|
|
"\t\t<last_ds>" . $rrd_info_ds_settings['last_ds'] . '</last_ds>' . PHP_EOL .
|
|
"\t\t<value>" . $rrd_info_ds_settings['value'] . '</value>' . PHP_EOL .
|
|
"\t\t<unknown_sec> " . $rrd_info_ds_settings['unknown_sec'] . ' </unknown_sec>' . PHP_EOL .
|
|
"\t</ds>" . PHP_EOL;
|
|
$rrd_new_ds_definition .= $rrd_new_ds_definition__ds_definition_template;
|
|
}
|
|
$i++;
|
|
}
|
|
|
|
$rrd_new_header .= $rrd_new_ds_definition;
|
|
$rrd_new_body = "\t<!-- Round Robin Archives -->" . PHP_EOL;
|
|
$rrd_new_body__ds_definition = '';
|
|
|
|
// create one fake entry to fill up gaps we do not have data for
|
|
if ($data_template_ds_counter == 1) {
|
|
$row_copy_fake = 'NaN';
|
|
} else {
|
|
$row_copy_fake = [];
|
|
|
|
for ($i = 0; $i < $data_template_ds_counter; $i++) {
|
|
$row_copy_fake[] = 'NaN';
|
|
}
|
|
}
|
|
|
|
// create new archives uses the data template definitions provided by Cacti and fill them up with existing data
|
|
|
|
foreach ($data_template_data_settings as $data_template__rra_settings) {
|
|
$defined_cf = $known_cfs[$data_template__rra_settings['consolidation_function_id']];
|
|
$defined_rows = $data_template__rra_settings['rows'];
|
|
$defined_xff = $data_template__rra_settings['x_files_factor'];
|
|
$defined_ppr = $data_template__rra_settings['steps'];
|
|
|
|
$defined_step = $data_template__rra_settings['steps'] * $rrd_info['step'];
|
|
// $defined_timespan = $data_template__rra_settings['timespan'];
|
|
$defined_timespan = $defined_rows * $defined_step;
|
|
|
|
$last_data_point = $rrd_info['last_update'] - $rrd_info['last_update'] % $defined_step;
|
|
$first_data_point = $last_data_point - $defined_timespan + $defined_step;
|
|
$timestamps = array_flip(range($first_data_point, $last_data_point, $defined_step));
|
|
|
|
$rrd_new_body .= "\t<rra>" . PHP_EOL .
|
|
"\t\t<cf>" . $defined_cf . '</cf>' . PHP_EOL .
|
|
"\t\t<pdp_per_row>" . $defined_ppr . '</pdp_per_row>' . PHP_EOL . PHP_EOL .
|
|
"\t\t<params>" . PHP_EOL .
|
|
"\t\t<xff>" . sprintf('%e', $defined_xff) . '</xff>' . PHP_EOL .
|
|
"\t\t</params>" . PHP_EOL .
|
|
"\t\t<cdp_prep>" . PHP_EOL;
|
|
|
|
$rrd_new_body__ds_definition = '';
|
|
|
|
for ($i = 0; $i < $data_template_ds_counter; $i++) {
|
|
$rrd_new_body__ds_definition_template = "\t\t\t<ds>" . PHP_EOL .
|
|
"\t\t\t<primary_value>__placeholder__$i</primary_value>" . PHP_EOL .
|
|
"\t\t\t<secondary_value>NaN</secondary_value>" . PHP_EOL .
|
|
"\t\t\t<value>NaN</value>" . PHP_EOL .
|
|
"\t\t\t<unknown_datapoints>0</unknown_datapoints>" . PHP_EOL .
|
|
"\t\t\t</ds>" . PHP_EOL;
|
|
$rrd_new_body__ds_definition .= $rrd_new_body__ds_definition_template;
|
|
}
|
|
|
|
$rrd_new_body .= $rrd_new_body__ds_definition .
|
|
"\t\t</cdp_prep>" . PHP_EOL .
|
|
"\t\t<database>" . PHP_EOL;
|
|
|
|
// initialize globals
|
|
$selected_archive_index = false;
|
|
$last_values = false;
|
|
$consolidation_required = false;
|
|
|
|
foreach ($timestamps as $timestamp => $dummy) {
|
|
$step = 9999999999;
|
|
|
|
$selected_archive_index = false;
|
|
|
|
foreach ($rra_timespans[$defined_cf] as $g_rra_index => $g_rra_settings) {
|
|
if ($g_rra_settings['step'] > $defined_step) {
|
|
if (($timestamp > $g_rra_settings['start']) &&
|
|
($timestamp <= $g_rra_settings['end']) &&
|
|
($g_rra_settings['step'] <= $step)) {
|
|
$consolidation_required = false;
|
|
$step = $g_rra_settings['step'];
|
|
$selected_archive_index = $g_rra_index;
|
|
}
|
|
} elseif ($g_rra_settings['step'] < $defined_step) {
|
|
if ((($timestamp - $defined_step) >= $g_rra_settings['start']) &&
|
|
($timestamp <= $g_rra_settings['end']) &&
|
|
($g_rra_settings['step'] <= $step)) {
|
|
$consolidation_required = true;
|
|
$step = $g_rra_settings['step'];
|
|
$selected_archive_index = $g_rra_index;
|
|
}
|
|
} else {
|
|
if (($timestamp > $g_rra_settings['start']) && ($timestamp <= $g_rra_settings['end'])) {
|
|
$consolidation_required = false;
|
|
$step = $g_rra_settings['step'];
|
|
$selected_archive_index = $g_rra_index;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// There's no data available we could reuse. Fill these entries with NaNs and jump to the next one
|
|
if ($selected_archive_index === false) {
|
|
$rrd_new_body .= "\t\t\t<!-- " . date('Y-m-d H:i:s ',
|
|
($timestamp)) . 'GMT / ' . ($timestamp) . ' --> <row><v>' . (is_array($row_copy_fake) ? implode('</v><v>',
|
|
$row_copy_fake) : $row_copy_fake) . '</v></row>' . PHP_EOL;
|
|
|
|
continue;
|
|
}
|
|
|
|
$g_rra_settings = $rra_timespans[$defined_cf][$selected_archive_index];
|
|
|
|
if (!$consolidation_required) {
|
|
// calculate the correct index number of the selected archive
|
|
$calculated_index = ($timestamp - $timestamp % $g_rra_settings['step'] - $g_rra_settings['start'] - $defined_step) / $g_rra_settings['step'];
|
|
|
|
if ($g_rra_settings['step'] > $defined_step && ($timestamp % $g_rra_settings['step']) == 0) {
|
|
$calculated_index--;
|
|
}
|
|
|
|
if (isset($rrd_data['rra'][$selected_archive_index]['database']['row'][$calculated_index]['v'])) {
|
|
$row_copy = $rrd_data['rra'][$selected_archive_index]['database']['row'][$calculated_index]['v'];
|
|
|
|
if (!empty($ds_superfluous_ids)) {
|
|
foreach ($ds_superfluous_ids as $ds_superfluous_id) {
|
|
unset($row_copy[$ds_superfluous_id]);
|
|
}
|
|
}
|
|
|
|
$rrd_new_body .= "\t\t\t<!-- " . date('Y-m-d H:i:s ',
|
|
($timestamp)) . 'GMT / ' . ($timestamp) . ' --> <row><v>' . (is_array($row_copy) ? implode('</v><v>',
|
|
$row_copy) : $row_copy) . '</v></row>' . PHP_EOL;
|
|
} else {
|
|
f_notify(false, "\033[0;31m[FAILED]\033[0m");
|
|
print_r($rra_timespans);
|
|
print $timestamp . '::' . $defined_cf;
|
|
|
|
// print $rrd_new_body;
|
|
// / log_missing
|
|
continue 4;
|
|
}
|
|
} else {
|
|
// data consolidation required, because we will start grepping data from an archive with a higher granularity
|
|
// calculate the correct index number of the selected archive
|
|
$calculated_index = ($timestamp - $timestamp % $g_rra_settings['step'] - $g_rra_settings['start'] - $defined_step) / $g_rra_settings['step'];
|
|
$high_granulary_rows_required = round($defined_step / $g_rra_settings['step'], 0);
|
|
$consolidated_ds_values = [];
|
|
|
|
if (is_array($rrd_data['rra'][$selected_archive_index]['database']['row'][$calculated_index]['v'])) {
|
|
foreach ($rrd_data['rra'][$selected_archive_index]['database']['row'][$calculated_index]['v'] as $ds_index => $ds_value) {
|
|
$unconsolidated_ds_values = [];
|
|
|
|
for ($i = 0; $i < $high_granulary_rows_required; $i++) {
|
|
$unconsolidated_ds_values[] = $rrd_data['rra'][$selected_archive_index]['database']['row'][$calculated_index + $i]['v'][$ds_index];
|
|
}
|
|
|
|
foreach ($unconsolidated_ds_values as $key => $value) {
|
|
if (!is_numeric($value) | is_null($value)) {
|
|
unset($unconsolidated_ds_values[$key]);
|
|
}
|
|
}
|
|
|
|
if (count($unconsolidated_ds_values) > 0) {
|
|
// drop all NaNs
|
|
|
|
switch ($defined_cf) {
|
|
case 'AVERAGE':
|
|
$consolidated_value = cacti_sizeof($unconsolidated_ds_values) ? 'NaN' : array_sum($unconsolidated_ds_values) / $high_granulary_rows_required;
|
|
|
|
break;
|
|
case 'MAX':
|
|
$consolidated_value = cacti_sizeof($unconsolidated_ds_values) ? 'NaN' : max($unconsolidated_ds_values);
|
|
|
|
break;
|
|
case 'MIN':
|
|
$consolidated_value = cacti_sizeof($unconsolidated_ds_values) ? 'NaN' : min($unconsolidated_ds_values);
|
|
|
|
break;
|
|
case 'LAST':
|
|
$consolidated_value = cacti_sizeof($unconsolidated_ds_values) ? 'NaN' : end($unconsolidated_ds_values);
|
|
|
|
break;
|
|
}
|
|
$consolidated_ds_values[$ds_index] = preg_replace('/(e[+-])(\d)$/',
|
|
'${1}0$2',
|
|
sprintf('%.10e',
|
|
$consolidated_value));
|
|
} else {
|
|
$consolidated_ds_values[$ds_index] = 'NaN';
|
|
}
|
|
}
|
|
} else {
|
|
$unconsolidated_ds_values = [];
|
|
|
|
for ($i = 0; $i < $high_granulary_rows_required; $i++) {
|
|
$unconsolidated_ds_values[] = $rrd_data['rra'][$selected_archive_index]['database']['row'][$calculated_index + $i]['v'];
|
|
}
|
|
|
|
foreach ($unconsolidated_ds_values as $key => $value) {
|
|
if (!is_numeric($value) | is_null($value)) {
|
|
unset($unconsolidated_ds_values[$key]);
|
|
}
|
|
}
|
|
|
|
if (count($unconsolidated_ds_values) > 0) {
|
|
// drop all NaNs
|
|
|
|
switch ($defined_cf) {
|
|
case 'AVERAGE':
|
|
$consolidated_value = cacti_sizeof($unconsolidated_ds_values) > 0 ? 'NaN' : array_sum($unconsolidated_ds_values) / $high_granulary_rows_required;
|
|
|
|
break;
|
|
case 'MAX':
|
|
$consolidated_value = cacti_sizeof($unconsolidated_ds_values) > 0 ? 'NaN' : max($unconsolidated_ds_values);
|
|
|
|
break;
|
|
case 'MIN':
|
|
$consolidated_value = cacti_sizeof($unconsolidated_ds_values) > 0 ? 'NaN' : min($unconsolidated_ds_values);
|
|
|
|
break;
|
|
case 'LAST':
|
|
$consolidated_value = cacti_sizeof($unconsolidated_ds_values) > 0 ? 'NaN' : end($unconsolidated_ds_values);
|
|
|
|
break;
|
|
}
|
|
$consolidated_ds_values[0] = preg_replace('/(e[+-])(\d)$/', '${1}0$2', sprintf('%.10e', $consolidated_value));
|
|
} else {
|
|
$consolidated_ds_values[0] = 'NaN';
|
|
}
|
|
}
|
|
|
|
if (!empty($ds_superfluous_ids)) {
|
|
foreach ($ds_superfluous_ids as $ds_superfluous_id) {
|
|
unset($consolidated_ds_values[$ds_superfluous_id]);
|
|
}
|
|
}
|
|
$rrd_new_body .= "\t\t\t<!-- " . date('Y-m-d H:i:s ',
|
|
($timestamp)) . 'GMT / ' . ($timestamp) . ' --> <row><v>' . implode('</v><v>',
|
|
$consolidated_ds_values) . '</v></row>' . PHP_EOL;
|
|
}
|
|
}
|
|
|
|
// return the last identified value
|
|
if ($selected_archive_index !== false) {
|
|
$last_values = (!isset($calculated_index)) ? 'NaN' : $rrd_data['rra'][$selected_archive_index]['database']['row'][$calculated_index]['v'];
|
|
}
|
|
|
|
if (is_array($last_values)) {
|
|
foreach ($last_values as $index => $value) {
|
|
$rrd_new_body = str_replace('>__placeholder__' . $index . '<', '>' . $value . '<', $rrd_new_body);
|
|
}
|
|
} else {
|
|
$rrd_new_body = str_replace('>__placeholder__0<', '>' . $last_values . '<', $rrd_new_body);
|
|
}
|
|
|
|
$rrd_new_body .= "\t\t</database>" . PHP_EOL .
|
|
"\t</rra>" . PHP_EOL;
|
|
}
|
|
f_notify(false, "\033[0;32m[COMPLETED]\033[0m");
|
|
|
|
// update temporary XML file
|
|
f_notify('Create Temp File');
|
|
|
|
if (!file_put_contents($tmp_xml_file, $rrd_new_header . $rrd_new_body . '</rrd>')) {
|
|
// Critical ERROR - This should never happen
|
|
f_notify(false, "\033[0;31m[FAILED]\033[0m");
|
|
f_log('[ERROR] Unable to create and update temporary XML file: ./tmp_rrdresize.xml');
|
|
|
|
if (!$debug_mode) {
|
|
f_notify(false, '[ERROR] Unable to create and update temporary XML file: ./tmp_rrdresize.xml', false);
|
|
}
|
|
|
|
exit;
|
|
} else {
|
|
f_notify(false, "\033[0;32m[COMPLETED]\033[0m");
|
|
}
|
|
|
|
// Use RRDtool to verify if XML structure is valid
|
|
f_notify('RRDtool Restore');
|
|
$file_restore = rrdtool_pipe_execute(' restore -r -f ' . $tmp_xml_file . ' ' . $tmp_rrd_file . "\r\n",
|
|
$rrdtool_pipes);
|
|
|
|
if (str_contains($file_restore, 'ERROR')) {
|
|
f_notify(false, "\033[0;31m[FAILED]\033[0m");
|
|
|
|
// / log_missing
|
|
continue;
|
|
} else {
|
|
f_notify(false, "\033[0;32m[COMPLETED]\033[0m");
|
|
|
|
// create host backup directory if it is still not existing
|
|
if (!is_dir($tmp_backup_folder . $host_id)) {
|
|
mkdir($tmp_backup_folder . $host_id);
|
|
}
|
|
|
|
f_notify('RRDtool Backup');
|
|
|
|
if (!copy($file, $tmp_backup_folder . $host_id . '/' . $host_file)) {
|
|
f_notify(false, "\033[0;31m[FAILED]\033[0m");
|
|
} else {
|
|
f_notify(false, "\033[0;32m[COMPLETED]\033[0m");
|
|
|
|
f_notify('RRDtool Replace');
|
|
|
|
if (!$dry_run_mode) {
|
|
if (!copy($tmp_rrd_file, $file) || !chmod($file, 0644)) {
|
|
f_notify(false, "\033[0;31m[FAILED]\033[0m");
|
|
} else {
|
|
f_notify(false, "\033[0;32m[COMPLETED]\033[0m");
|
|
}
|
|
} else {
|
|
f_notify(false, "\033[0;36m[SKIPPED]\033[0m");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$debug_mode) {
|
|
fwrite(STDOUT, '.');
|
|
}
|
|
$counter++;
|
|
|
|
if ($counter % 1000 == 0) {
|
|
fwrite(STDOUT, (string) $counter);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$debug_mode) {
|
|
fwrite(STDOUT, "\r\n");
|
|
}
|
|
|
|
exit;
|
|
|
|
function rrdtool_parse_info(string $lines) : array {
|
|
$store = [];
|
|
$lines = explode(PHP_EOL, trim($lines));
|
|
|
|
foreach ($lines as $line) {
|
|
[$raw_key, $raw_val] = explode(' = ', $line);
|
|
|
|
$keys = preg_split('/[\.\[\]]/', $raw_key, -1, PREG_SPLIT_NO_EMPTY);
|
|
$key_count = count($keys);
|
|
$pointer = &$store;
|
|
|
|
foreach (($keys ?: []) as $key_num => $key) {
|
|
$pointer = &$pointer[$key];
|
|
|
|
if ($key_num + 1 === $key_count) {
|
|
$pointer = trim($raw_val, '"');
|
|
}
|
|
}
|
|
}
|
|
|
|
return $store;
|
|
}
|
|
|
|
function rrdtool_pipe_init(string $path_rrdtool) : array {
|
|
$fds = [
|
|
0 => ['pipe', 'r'], // stdin
|
|
1 => ['pipe', 'w'], // stdout
|
|
2 => ['file', '/dev/null', 'a'] // stderr
|
|
];
|
|
$process = proc_open($path_rrdtool . ' -', $fds, $pipes);
|
|
|
|
// make stdin/stdout/stderr non-blocking
|
|
stream_set_blocking($pipes[0], false);
|
|
stream_set_blocking($pipes[1], false);
|
|
|
|
return [$process, $pipes];
|
|
}
|
|
|
|
function rrdtool_pipe_close(mixed $process) : void {
|
|
proc_close($process);
|
|
}
|
|
|
|
function rrdtool_pipe_execute(string $command, mixed $pipes) : string {
|
|
$stdout = '';
|
|
$return_code = fwrite($pipes[0], $command);
|
|
|
|
while (!feof($pipes[1])) {
|
|
$line = fgets($pipes[1], 8092);
|
|
|
|
// return the complete output cause the logic behind a proxy should be as simple as possible
|
|
if (substr_count((string) $line, 'OK u')) {
|
|
break;
|
|
}
|
|
$stdout .= $line;
|
|
}
|
|
|
|
return $stdout;
|
|
}
|
|
|
|
function dirToArrayOne(string $dir) : array {
|
|
global $total_files;
|
|
|
|
$files = 0;
|
|
$folders = 0;
|
|
|
|
$result = [];
|
|
|
|
$cdir = scandir($dir);
|
|
|
|
if ($cdir) {
|
|
foreach ($cdir as $key => $value) {
|
|
if (!in_array($value, ['.', '..'], true)) {
|
|
if (is_dir($dir . DIRECTORY_SEPARATOR . $value)) {
|
|
$folders++;
|
|
$result[$value] = dirToArrayOne($dir . DIRECTORY_SEPARATOR . $value);
|
|
} else {
|
|
if (substr($value, -4) == '.rrd') {
|
|
$result[] = $value;
|
|
$files++;
|
|
$total_files++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $summary = ['files' => $files, 'folders' => $folders, 'content' => $result];
|
|
}
|
|
|
|
function f_notify(mixed $category = false, mixed $status = false, bool $debug_mode_only = true) : bool {
|
|
global $debug_mode;
|
|
|
|
if (!$debug_mode && $debug_mode_only) {
|
|
return true;
|
|
} else {
|
|
if ($category && $status) {
|
|
fwrite(STDOUT, sprintf(" %-20s: %s\r\n", $category, $status));
|
|
} elseif (!$category && $status) {
|
|
fwrite(STDOUT, sprintf("%s\r\n", $status));
|
|
} elseif ($category && $status === false) {
|
|
fwrite(STDOUT, sprintf(' %-20s: ', $category));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function f_log(string $msg) : void {
|
|
global $logging, $log_handle, $total_outdated, $total_errors, $total_skipped, $total_mismatches;
|
|
|
|
fwrite($log_handle, date('Y-m-d H:i:s T', time()) . ' ' . $msg . PHP_EOL);
|
|
|
|
if (str_contains($msg, '[ERROR]')) {
|
|
$total_errors++;
|
|
$total_skipped++;
|
|
} elseif (str_contains($msg, '[OUTDATED]')) {
|
|
$total_outdated++;
|
|
$total_skipped++;
|
|
} elseif (str_contains($msg, '[SKIPPED]')) {
|
|
$total_skipped++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* display_version - displays version information
|
|
*
|
|
* @return void
|
|
*/
|
|
function display_version() : void {
|
|
$version = get_cacti_cli_version();
|
|
print "Cacti RRDfile Reassign Data Template, Version $version, " . COPYRIGHT_YEARS . PHP_EOL;
|
|
}
|
|
|
|
/**
|
|
* display_help - displays help information
|
|
*
|
|
* @return void
|
|
*/
|
|
function display_help() : void {
|
|
display_version();
|
|
|
|
print PHP_EOL;
|
|
print 'A simple command line utility to analyse and reassign data template settings' . PHP_EOL;
|
|
print 'into RRDfiles based upon.' . PHP_EOL . PHP_EOL;
|
|
|
|
print 'usage: rrdresize.php --data-template-id=[ID] --backup=[PATH] [--dry-run] [--debug, -d]' . PHP_EOL . PHP_EOL;
|
|
|
|
print 'Required:' . PHP_EOL;
|
|
print ' --data-template-id the numerical ID of the host' . PHP_EOL;
|
|
print ' --backup path to backup original RRDfiles' . PHP_EOL . PHP_EOL;
|
|
|
|
print 'Optional:' . PHP_EOL;
|
|
print ' --dry-run no boost cache update and no replacement of rrdfiles' . PHP_EOL;
|
|
print ' --debug, -d show detail processing data' . PHP_EOL;
|
|
print ' --list-data-templates list all data templates' . PHP_EOL . PHP_EOL;
|
|
}
|