mirror of
https://github.com/Cacti/rrdproxy.git
synced 2026-10-25 12:36:43 +09:00
* Harden RRDtool proxy request handling * Address PR review feedback: encrypt oversized-request errors, fix env inheritance, spacing, and symlink test Addresses review comments on PR #17: - lib/client.php: encrypt the 'Request too large' error frame once the client is authenticated, matching the existing encrypted-error pattern - lib/client.php: build the removespikes child-process environment from getenv() instead of $_ENV so the full current environment (e.g. TEMP on Windows) is inherited, with RRDP_RRDTOOL_PATH merged on top - lib/client.php: add the missing space in the RRDTOOL_PIPE_UNAVAILABLE error frame so it matches the other 'ERROR: ...' responses - tests/run.php: check the symlink() fixture return value and skip the symlink-escape assertion (instead of producing a false positive) when symlink() is unavailable * Upgrade phpseclib 3.x to phpseclib/phpseclib ^4.0 - composer.json/composer.lock: require phpseclib/phpseclib ^4.0 (was ^3.0) - Update phpseclib3\\... namespace usages to phpseclib4\\... across rrdtool-proxy.php, lib/client.php, lib/functions.php, lib/wizard.php, and tests/run.php - lib/functions.php: phpseclib4 removed the Random class, so Random::string(32) is replaced with random_bytes(32) - Fix lib/__master__.php and lib/replicator.php, which PR #17 never migrated off the old pre-Composer phpseclib API (new \\phpseclib\\Crypt\\RSA(), ->loadKey(), ->getPublicKeyFingerprint() -- none of which exist in phpseclib 3.x or 4.x, so these MSR/replicator peer-authentication code paths would fatal at runtime). Both now use RSA::loadPublicKey()->getFingerprint('md5') wrapped in try/catch, matching the pattern already used in lib/client.php, plus a constant-time hash_equals() fingerprint comparison - tests/run.php: relabel the phpseclib round-trip check from 'phpseclib 3' to 'phpseclib 4' * Bump minimum PHP version to 8.2 and add CHANGELOG.md - composer.json: require php ^8.2 (was ^8.1) - include/global.php: bump RRDP_PHP_VERSION_REQUIRED to 80200 to match - README.md: update minimum/recommended PHP version to 8.2 - composer.lock: refreshed for the new php platform constraint - Add CHANGELOG.md tracking changes prefixed security:/issue:/feature: * Add PHPDoc blocks to all functions Adds a PHPDoc block above every function in rrdtool-proxy.php, lib/functions.php, lib/client.php, lib/__master__.php, lib/replicator.php, lib/wizard.php, cli/removespikes.php, tests/run.php, and .phpstan-bootstrap.php, each with a brief description and inferred @param/@return types. Function signatures themselves are unchanged; PHPStan will flag any inferred types that turn out to be inaccurate. * Add .github/copilot-instructions.md Generated from README.md and this session's learnings: project architecture (daemon entrypoint, shared lib helpers, client/master/replicator roles, wizard, removespikes CLI), build/lint/test commands (composer scripts, php tests/run.php, Windows platform-req caveats), and conventions (tabs, GPL header block, PHPDoc-only typing, phpseclib4 gotchas, CHANGELOG entry prefixes). Also fix .gitignore: the '.git*' pattern was unintentionally also matching and ignoring the .github/ directory; added a '!.github/' exception so files under .github/ (like this one) are tracked. * Add .gitattributes enforcing LF line endings; normalize existing CRLF files - Add .gitattributes with '* text=auto eol=lf' so Git normalizes line endings to LF on checkout/commit regardless of local core.autocrlf settings. - .gitignore: add '!.gitattributes' alongside the existing '!.github/' exception, since '.git*' was also unintentionally ignoring .gitattributes itself. - Normalize the two tracked files that had CRLF line endings (.github/copilot-instructions.md, CHANGELOG.md) to LF; no other tracked files were affected. * Manual PSR coding-style adjustments Manual formatting/coding-style cleanup in cli/removespikes.php, lib/__master__.php, lib/client.php, and lib/replicator.php. * Address automated review feedback: symlink confinement, key-pair validation, and misc fixes Addresses 10 unresolved copilot-pull-request-reviewer comments on PR #20: - lib/functions.php: rewrite rrdp_resolve_path_within() to walk the path one segment at a time (instead of only checking the nearest existing ancestor via file_exists()), so a symlink anywhere along a not-yet-fully-existing path is resolved and confined even if the symlink itself is dangling. - lib/functions.php: add rrdp_resolve_command_paths(), which resolves every RRD file path operand referenced by an RRDtool command (leading bare file arguments and DEF:/SDEF: clauses) against the RRA root and rewrites the command to use the canonical resolved paths, closing the symlink-escape gap left by the lexical-only rrdp_command_has_unsafe_path() check. - lib/client.php: wire rrdp_resolve_command_paths() into the RRDtool command dispatch path, rejecting/rewriting unsafe operands before they reach the RRDtool pipe. - lib/client.php: enforce the request-size limit on each completed transaction and the trailing unfinished remainder, instead of on the aggregate receive buffer before splitting on _EOT_, so multiple legitimate transactions in one read no longer trip a false 'Request too large'. - rrdtool-proxy.php: validate at startup that the loaded public/private key files actually form a matching pair (comparing derived fingerprints) instead of only deriving a fingerprint from the public key, failing startup instead of accepting requests that will all fail to decrypt. - lib/__master__.php, lib/replicator.php: resolve peer-supplied FULLSCAN folder/file names (and RRDLAST/RRDDUMP request filenames) through rrdp_resolve_rra_path() before using them in mkdir()/touch()/ file_put_contents()/RRDtool restore-dump-last-flushcached operations, instead of concatenating raw peer-supplied text; reject and skip entries that fail confinement. - lib/replicator.php, lib/__master__.php: guard rrdtool_pipe_init() failures in the main replication loop the same way lib/client.php already does, instead of indexing into a false return value and never retrying. - cli/removespikes.php: make createRRDFileFromXML() return whether the RRDtool restore actually succeeded, and exit non-zero when it fails instead of silently exiting 0. - .phpstan.neon: restore level 6 (was lowered to 2, contradicting .github/copilot-instructions.md) and add .phpstan-baseline.neon to capture the 42 pre-existing findings surfaced at level 6 without hiding future regressions. - tests/run.php: add the standard Cacti Group GPL header block required for every PHP file. --------- Co-authored-by: Thomas Vincent <thomasvincent@gmail.com>
1056 lines
34 KiB
PHP
1056 lines
34 KiB
PHP
#!/usr/bin/php -q
|
|
<?php
|
|
/*
|
|
ex: set tabstop=4 shiftwidth=4 autoindent:
|
|
+-------------------------------------------------------------------------+
|
|
| 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/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
// do NOT run this script through a web browser
|
|
if (!isset($_SERVER['argv'][0]) || isset($_SERVER['REQUEST_METHOD']) || isset($_SERVER['REMOTE_ADDR'])) {
|
|
die('<br><strong>This script is only meant to run at the command line.</strong>');
|
|
}
|
|
|
|
// We are not talking to the browser
|
|
$no_http_headers = true;
|
|
|
|
$dir = __DIR__;
|
|
chdir($dir);
|
|
|
|
if (strpos($dir, 'spikekill') !== false) {
|
|
chdir('../../');
|
|
}
|
|
|
|
// Start Initialization Section
|
|
if (file_exists('./include/global.php') & !file_exists('../rrdtool-proxy.php')) {
|
|
include_once('./include/global.php');
|
|
$using_cacti = true;
|
|
} else {
|
|
$using_cacti = false;
|
|
}
|
|
|
|
// setup defaults
|
|
$debug = false;
|
|
$dryrun = false;
|
|
$avgnan = 'avg';
|
|
$rrdfile = '';
|
|
$std_kills = false;
|
|
$var_kills = false;
|
|
$html = false;
|
|
$backup = false;
|
|
$config = [];
|
|
$new_output = [];
|
|
|
|
if ($using_cacti) {
|
|
$method = read_config_option('spikekill_method');
|
|
$numspike = read_config_option('spikekill_number');
|
|
$stddev = read_config_option('spikekill_deviations');
|
|
$percent = read_config_option('spikekill_percent');
|
|
$outliers = read_config_option('spikekill_outliers');
|
|
} else {
|
|
$method = 1; // Standard Deviation
|
|
$numspike = 100;
|
|
$stddev = 10;
|
|
$percent = 500;
|
|
$outliers = 5;
|
|
}
|
|
|
|
// process calling arguments
|
|
$parms = $_SERVER['argv'];
|
|
array_shift($parms);
|
|
|
|
foreach ($parms as $parameter) {
|
|
@[$arg, $value] = @explode('=', $parameter);
|
|
|
|
switch ($arg) {
|
|
case '--method':
|
|
case '-M':
|
|
if ($value == 'variance') {
|
|
$method = 2;
|
|
} elseif ($value == 'stddev') {
|
|
$method = 1;
|
|
} else {
|
|
print "FATAL: You must specify either 'stddev' or 'variance' as methods.\n\n";
|
|
display_help();
|
|
exit(-11);
|
|
}
|
|
|
|
break;
|
|
case '--avgnan':
|
|
case '-A':
|
|
if ($value == 'avg') {
|
|
$avgnan = 'avg';
|
|
} elseif ($value == 'nan') {
|
|
$avgnan = 'nan';
|
|
} else {
|
|
print "FATAL: You must specify either 'avg' or 'nan' as replacement methods.\n\n";
|
|
display_help();
|
|
exit(-10);
|
|
}
|
|
|
|
break;
|
|
case '--rrdfile':
|
|
case '-R':
|
|
$rrdfile = $value;
|
|
|
|
if (!file_exists($rrdfile)) {
|
|
print "FATAL: File '$rrdfile' does not exist.\n";
|
|
exit(-9);
|
|
}
|
|
|
|
if (!is_writable($rrdfile)) {
|
|
print "FATAL: File '$rrdfile' is not writable by this account.\n";
|
|
exit(-8);
|
|
}
|
|
|
|
break;
|
|
case '--stddev':
|
|
case '-S':
|
|
$stddev = $value;
|
|
|
|
if (!is_numeric($stddev) || ($stddev < 1)) {
|
|
print "FATAL: Standard Deviation must be a positive integer.\n\n";
|
|
display_help();
|
|
exit(-7);
|
|
}
|
|
|
|
break;
|
|
case '--outliers':
|
|
case '-O':
|
|
$outliers = $value;
|
|
|
|
if (!is_numeric($outliers) || ($outliers < 1)) {
|
|
print "FATAL: The number of outliers to exclude must be a positive integer.\n\n";
|
|
display_help();
|
|
exit(-6);
|
|
}
|
|
|
|
break;
|
|
case '--percent':
|
|
case '-P':
|
|
$percent = $value;
|
|
|
|
if (!is_numeric($percent) || ($percent <= 0)) {
|
|
print "FATAL: Percent deviation must be a positive floating point number.\n\n";
|
|
display_help();
|
|
exit(-5);
|
|
}
|
|
|
|
break;
|
|
case '--html':
|
|
$html = true;
|
|
|
|
break;
|
|
case '--backup':
|
|
$backup = true;
|
|
|
|
break;
|
|
case '-d':
|
|
case '--debug':
|
|
$debug = true;
|
|
|
|
break;
|
|
case '-D':
|
|
case '--dryrun':
|
|
$dryrun = true;
|
|
|
|
break;
|
|
case '--number':
|
|
case '-N':
|
|
case '-n':
|
|
$numspike = $value;
|
|
|
|
if (!is_numeric($numspike) || ($numspike < 1)) {
|
|
print "FATAL: Number of spikes to remove must be a positive integer\n\n";
|
|
display_help();
|
|
exit(-4);
|
|
}
|
|
|
|
break;
|
|
case '-h':
|
|
case '-v':
|
|
case '-V':
|
|
case '--version':
|
|
case '--help':
|
|
display_help();
|
|
exit(0);
|
|
default:
|
|
print 'ERROR: Invalid Parameter ' . $parameter . "\n\n";
|
|
display_help();
|
|
exit(-3);
|
|
}
|
|
}
|
|
|
|
// additional error check
|
|
if ($rrdfile == '') {
|
|
print "FATAL: You must specify an RRDfile!\n\n";
|
|
display_help();
|
|
exit(-2);
|
|
}
|
|
|
|
// let's see if we can find rrdtool
|
|
$rrdtool_path = getenv('RRDP_RRDTOOL_PATH');
|
|
|
|
if ($rrdtool_path === false || $rrdtool_path === '') {
|
|
$rrdtool_path = $using_cacti ? read_config_option('path_rrdtool') : (substr_count(PHP_OS, 'WIN') ? 'rrdtool.exe' : 'rrdtool');
|
|
}
|
|
|
|
if (!$using_cacti) {
|
|
$response = removespikes_run_rrdtool($rrdtool_path, []);
|
|
|
|
if ($response !== false && strlen($response['stdout'] . $response['stderr'])) {
|
|
$response = $response['stdout'] . $response['stderr'];
|
|
$response_array = explode(' ', $response);
|
|
print 'NOTE: Using ' . $response_array[0] . ' Version ' . $response_array[1] . "\n";
|
|
} else {
|
|
print "FATAL: RRDTool not found in path. Please insure RRDTool can be found in your path!\n";
|
|
exit(-1);
|
|
}
|
|
}
|
|
|
|
// determine the temporary file name
|
|
$seed = mt_rand();
|
|
|
|
if ($using_cacti) {
|
|
if ($config['cacti_server_os'] == 'win32') {
|
|
$tempdir = getenv('TEMP');
|
|
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
|
|
$bakfile = $tempdir . '/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
|
|
} else {
|
|
$tempdir = '/tmp';
|
|
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
|
|
$bakfile = '/tmp/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
|
|
}
|
|
} elseif (substr_count(PHP_OS, 'WIN')) {
|
|
$tempdir = getenv('TEMP');
|
|
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
|
|
$bakfile = $tempdir . '/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
|
|
} else {
|
|
$tempdir = '/tmp';
|
|
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
|
|
$bakfile = '/tmp/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
|
|
}
|
|
|
|
if ($xmlfile === false) {
|
|
print "FATAL: Unable to create a secure temporary file.\n";
|
|
exit(-12);
|
|
}
|
|
|
|
if ($html) {
|
|
print "<table cellpadding='3' cellspacing='0' class='spikekill_data' id='spikekill_data'>";
|
|
}
|
|
|
|
if ($using_cacti) {
|
|
cacti_log("NOTE: Removing Spikes for '$rrdfile', Method:'$method'", false, 'WEBUI');
|
|
}
|
|
|
|
// execute the dump command
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "NOTE: Creating XML file '$xmlfile' from '$rrdfile'" . ($html ? "</td></tr>\n" : "\n");
|
|
|
|
$dump_result = removespikes_run_rrdtool($rrdtool_path, ['dump', $rrdfile]);
|
|
|
|
if ($dump_result !== false && $dump_result['status'] === 0 && $dump_result['stdout'] !== '') {
|
|
file_put_contents($xmlfile, $dump_result['stdout'], LOCK_EX);
|
|
} else {
|
|
unlink($xmlfile);
|
|
}
|
|
|
|
// read the xml file into an array
|
|
if (file_exists($xmlfile)) {
|
|
$output = file($xmlfile);
|
|
|
|
// remove the temp file
|
|
unlink($xmlfile);
|
|
} else {
|
|
if ($using_cacti) {
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . 'FATAL: RRDtool Command Failed. Please verify that the RRDtool path is valid in Settings->Paths!' . ($html ? "</td></tr>\n" : "\n");
|
|
} else {
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . 'FATAL: RRDtool Command Failed. Please insure your RRDtool install is valid!' . ($html ? "</td></tr>\n" : "\n");
|
|
}
|
|
exit(-12);
|
|
}
|
|
|
|
// backup the rrdfile if requested
|
|
if ($backup && !$dryrun) {
|
|
if (copy($rrdfile, $bakfile)) {
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "NOTE: RRDfile '$rrdfile' backed up to '$bakfile'" . ($html ? "</td></tr>\n" : "\n");
|
|
} else {
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "FATAL: RRDfile Backup of '$rrdfile' to '$bakfile' FAILED!" . ($html ? "</td></tr>\n" : "\n");
|
|
exit(-13);
|
|
}
|
|
}
|
|
|
|
// process the xml file and remove all comments
|
|
$output = removeComments($output);
|
|
|
|
/* Read all the rra's ds values and obtain the following pieces of information from each
|
|
rra archive.
|
|
|
|
* numsamples - The number of 'valid' non-nan samples
|
|
* sumofsamples - The sum of all 'valid' samples.
|
|
* average - The average of all samples
|
|
* standard_deviation - The standard deviation of all samples
|
|
* max_value - The maximum value of all samples
|
|
* min_value - The minimum value of all samples
|
|
* max_cutoff - Any value above this value will be set to the average.
|
|
* min_cutoff - Any value lower than this value will be set to the average.
|
|
|
|
This will end up being a n-dimensional array as follows:
|
|
rra[x][ds#]['totalsamples'];
|
|
rra[x][ds#]['numsamples'];
|
|
rra[x][ds#]['sumofsamples'];
|
|
rra[x][ds#]['average'];
|
|
rra[x][ds#]['stddev'];
|
|
rra[x][ds#]['max_value'];
|
|
rra[x][ds#]['min_value'];
|
|
rra[x][ds#]['max_cutoff'];
|
|
rra[x][ds#]['min_cutoff'];
|
|
|
|
There will also be a secondary array created with the actual samples. This
|
|
array will be used to calculate the standard deviation of the sample set.
|
|
samples[rra_num][ds_num][];
|
|
|
|
Also track the min and max value for each ds and store it into the two
|
|
arrays: ds_min[ds#], ds_max[ds#].
|
|
|
|
The we don't need to know the type of rra, only it's number for this analysis
|
|
the same applies for the ds' as well.
|
|
*/
|
|
$rra = [];
|
|
$rra_cf = [];
|
|
$rra_pdp = [];
|
|
$rra_num = 0;
|
|
$ds_num = 0;
|
|
$total_kills = 0;
|
|
$in_rra = false;
|
|
$in_db = false;
|
|
$ds_min = [];
|
|
$ds_max = [];
|
|
$ds_name = [];
|
|
|
|
/* perform a first pass on the array and do the following:
|
|
1) Get the number of good samples per ds
|
|
2) Get the sum of the samples per ds
|
|
3) Get the max and min values for all samples
|
|
4) Build both the rra and sample arrays
|
|
5) Get each ds' min and max values
|
|
*/
|
|
if (sizeof($output)) {
|
|
foreach ($output as $line) {
|
|
if (substr_count($line, '<v>')) {
|
|
$linearray = explode('<v>', $line);
|
|
// discard the row
|
|
array_shift($linearray);
|
|
$ds_num = 0;
|
|
|
|
foreach ($linearray as $dsvalue) {
|
|
// peel off garbage
|
|
$dsvalue = trim(str_replace('</row>', '', str_replace('</v>', '', $dsvalue)));
|
|
|
|
if (strtolower($dsvalue) != 'nan') {
|
|
if (!isset($rra[$rra_num][$ds_num]['numsamples'])) {
|
|
$rra[$rra_num][$ds_num]['numsamples'] = 1;
|
|
} else {
|
|
$rra[$rra_num][$ds_num]['numsamples']++;
|
|
}
|
|
|
|
if (!isset($rra[$rra_num][$ds_num]['sumofsamples'])) {
|
|
$rra[$rra_num][$ds_num]['sumofsamples'] = $dsvalue;
|
|
} else {
|
|
$rra[$rra_num][$ds_num]['sumofsamples'] += $dsvalue;
|
|
}
|
|
|
|
if (!isset($rra[$rra_num][$ds_num]['max_value'])) {
|
|
$rra[$rra_num][$ds_num]['max_value'] = $dsvalue;
|
|
} elseif ($dsvalue > $rra[$rra_num][$ds_num]['max_value']) {
|
|
$rra[$rra_num][$ds_num]['max_value'] = $dsvalue;
|
|
}
|
|
|
|
if (!isset($rra[$rra_num][$ds_num]['min_value'])) {
|
|
$rra[$rra_num][$ds_num]['min_value'] = $dsvalue;
|
|
} elseif ($dsvalue < $rra[$rra_num][$ds_num]['min_value']) {
|
|
$rra[$rra_num][$ds_num]['min_value'] = $dsvalue;
|
|
}
|
|
|
|
// store the sample for standard deviation calculation
|
|
$samples[$rra_num][$ds_num][] = $dsvalue;
|
|
}
|
|
|
|
if (!isset($rra[$rra_num][$ds_num]['totalsamples'])) {
|
|
$rra[$rra_num][$ds_num]['totalsamples'] = 1;
|
|
} else {
|
|
$rra[$rra_num][$ds_num]['totalsamples']++;
|
|
}
|
|
|
|
$ds_num++;
|
|
}
|
|
} elseif (substr_count($line, '<rra>')) {
|
|
$in_rra = true;
|
|
} elseif (substr_count($line, '<min>')) {
|
|
$ds_min[] = trim(str_replace('<min>', '', str_replace('</min>', '', trim($line))));
|
|
} elseif (substr_count($line, '<max>')) {
|
|
$ds_max[] = trim(str_replace('<max>', '', str_replace('</max>', '', trim($line))));
|
|
} elseif (substr_count($line, '<name>')) {
|
|
$ds_name[] = trim(str_replace('<name>', '', str_replace('</name>', '', trim($line))));
|
|
} elseif (substr_count($line, '<cf>')) {
|
|
$rra_cf[] = trim(str_replace('<cf>', '', str_replace('</cf>', '', trim($line))));
|
|
} elseif (substr_count($line, '<pdp_per_row>')) {
|
|
$rra_pdp[] = trim(str_replace('<pdp_per_row>', '', str_replace('</pdp_per_row>', '', trim($line))));
|
|
} elseif (substr_count($line, '</rra>')) {
|
|
$in_rra = false;
|
|
$rra_num++;
|
|
} elseif (substr_count($line, '<step>')) {
|
|
$step = trim(str_replace('<step>', '', str_replace('</step>', '', trim($line))));
|
|
}
|
|
}
|
|
}
|
|
|
|
// For all the samples determine the average with the outliers removed
|
|
calculateVarianceAverages($rra, $samples);
|
|
|
|
/* Now scan the rra array and the samples array and calculate the following
|
|
1) The standard deviation of all samples
|
|
2) The average of all samples per ds
|
|
3) The max and min cutoffs of all samples
|
|
4) The number of kills in each ds based upon the thresholds
|
|
*/
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "NOTE: Searching for Spikes in XML file '$xmlfile'" . ($html ? "</td></tr>\n" : "\n");
|
|
calculateOverallStatistics($rra, $samples);
|
|
|
|
// debugging and/or status report
|
|
if ($debug || $dryrun) {
|
|
outputStatistics($rra);
|
|
}
|
|
|
|
// create an output array
|
|
if ($method == 1) {
|
|
// standard deviation subroutine
|
|
if ($std_kills) {
|
|
if (!$dryrun) {
|
|
$new_output = updateXML($output, $rra);
|
|
}
|
|
} else {
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "NOTE: NO Standard Deviation Spikes found in '$rrdfile'" . ($html ? "</td></tr>\n" : "\n");
|
|
}
|
|
} else {
|
|
// variance subroutine
|
|
if ($var_kills) {
|
|
if (!$dryrun) {
|
|
$new_output = updateXML($output, $rra);
|
|
}
|
|
} else {
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "NOTE: NO Variance Spikes found in '$rrdfile'" . ($html ? "</td></tr>\n" : "\n");
|
|
}
|
|
}
|
|
|
|
// finally update the file XML file and Reprocess the RRDfile
|
|
if (!$dryrun) {
|
|
if ($total_kills) {
|
|
if (writeXMLFile($new_output, $xmlfile)) {
|
|
if (backupRRDFile($rrdfile)) {
|
|
if (!createRRDFileFromXML($xmlfile, $rrdfile)) {
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "FATAL: Unable to restore '$rrdfile' from '$xmlfile'" . ($html ? "</td></tr>\n" : "\n");
|
|
exit(-14);
|
|
}
|
|
} else {
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "FATAL: Unable to backup '$rrdfile'" . ($html ? "</td></tr>\n" : "\n");
|
|
}
|
|
} else {
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "FATAL: Unable to write XML file '$xmlfile'" . ($html ? "</td></tr>\n" : "\n");
|
|
}
|
|
}
|
|
} else {
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . 'NOTE: Dryrun requested. No updates performed' . ($html ? "</td></tr>\n" : "\n");
|
|
}
|
|
|
|
if ($html) {
|
|
print '</table>';
|
|
}
|
|
|
|
// All Functions
|
|
/**
|
|
* Restores an RRD file from its (spike-cleaned) XML dump via `rrdtool restore`.
|
|
*
|
|
* @param string $xmlfile
|
|
* @param string $rrdfile
|
|
*
|
|
* @return bool
|
|
*/
|
|
function createRRDFileFromXML($xmlfile, $rrdfile) {
|
|
global $html, $rrdtool_path;
|
|
|
|
// execute the dump command
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "NOTE: Re-Importing '$xmlfile' to '$rrdfile'" . ($html ? "</td></tr>\n" : "\n");
|
|
|
|
$result = removespikes_run_rrdtool($rrdtool_path, ['restore', '-f', '-r', $xmlfile, $rrdfile]);
|
|
$response = $result === false ? 'Unable to start RRDtool' : $result['stdout'] . $result['stderr'];
|
|
|
|
if (strlen($response)) {
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . $response . ($html ? "</td></tr>\n" : "\n");
|
|
}
|
|
|
|
return $result !== false && $result['status'] === 0;
|
|
}
|
|
|
|
/**
|
|
* Runs the rrdtool binary with the given argv-style arguments (no shell
|
|
* interpolation) and captures its combined stdout/stderr and exit status.
|
|
*
|
|
* @param string $binary
|
|
* @param array<int, string> $arguments
|
|
*
|
|
* @return array{status: int, stdout: string, stderr: string}|false
|
|
*/
|
|
function removespikes_run_rrdtool($binary, $arguments) {
|
|
$descriptors = [
|
|
1 => ['pipe', 'w'],
|
|
2 => ['redirect', 1],
|
|
];
|
|
$process = proc_open(array_merge([$binary], $arguments), $descriptors, $pipes);
|
|
|
|
if (!is_resource($process)) {
|
|
return false;
|
|
}
|
|
|
|
$stdout = stream_get_contents($pipes[1]);
|
|
fclose($pipes[1]);
|
|
$status = proc_close($process);
|
|
|
|
return ['status' => $status, 'stdout' => $stdout, 'stderr' => ''];
|
|
}
|
|
|
|
/**
|
|
* Writes the (spike-cleaned) XML dump lines out to $xmlfile.
|
|
*
|
|
* @param array<int, string> $output
|
|
* @param string $xmlfile
|
|
*
|
|
* @return int|false
|
|
*/
|
|
function writeXMLFile($output, $xmlfile) {
|
|
return file_put_contents($xmlfile, $output);
|
|
}
|
|
|
|
/**
|
|
* Copies $rrdfile into the configured (or temp) backup directory before it is
|
|
* modified, appending a random seed to the filename if a file of that name
|
|
* already exists there.
|
|
*
|
|
* @param string $rrdfile
|
|
*
|
|
* @return bool
|
|
*/
|
|
function backupRRDFile($rrdfile) {
|
|
global $using_cacti, $tempdir, $seed, $html;
|
|
|
|
if ($using_cacti) {
|
|
$backupdir = read_config_option('spikekill_backupdir');
|
|
|
|
if ($backupdir == '') {
|
|
$backupdir = $tempdir;
|
|
}
|
|
} else {
|
|
$backupdir = $tempdir;
|
|
}
|
|
|
|
if (file_exists($backupdir . '/' . basename($rrdfile))) {
|
|
$newfile = basename($rrdfile) . '.' . $seed;
|
|
} else {
|
|
$newfile = basename($rrdfile);
|
|
}
|
|
|
|
print ($html ? "<tr><td colspan='20' class='spikekill_note'>" : '') . "NOTE: Backing Up '$rrdfile' to '" . $backupdir . '/' . $newfile . "'" . ($html ? "</td></tr>\n" : "\n");
|
|
|
|
return copy($rrdfile, $backupdir . '/' . $newfile);
|
|
}
|
|
|
|
/**
|
|
* Computes each RRA/data-source's outlier-trimmed "variance" average (the mean
|
|
* of the samples after removing the top/bottom $outliers extremes), storing it
|
|
* into $rra[$rra_num][$ds_num]['variance_avg'].
|
|
*
|
|
* @param array<int, array<int, array<string, mixed>>> $rra
|
|
* @param array<int, array<int, array<int, float|int>>> $samples
|
|
*
|
|
* @return void
|
|
*/
|
|
function calculateVarianceAverages(&$rra, &$samples) {
|
|
global $outliers;
|
|
|
|
if (sizeof($samples)) {
|
|
foreach ($samples as $rra_num => $dses) {
|
|
if (sizeof($dses)) {
|
|
foreach ($dses as $ds_num => $ds) {
|
|
if (sizeof($ds) < $outliers * 3) {
|
|
$rra[$rra_num][$ds_num]['variance_avg'] = 'NAN';
|
|
} else {
|
|
rsort($ds, SORT_NUMERIC);
|
|
$ds = array_slice($ds, $outliers);
|
|
|
|
sort($ds, SORT_NUMERIC);
|
|
$ds = array_slice($ds, $outliers);
|
|
|
|
$rra[$rra_num][$ds_num]['variance_avg'] = array_sum($ds) / sizeof($ds);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For every RRA/data-source, computes the standard deviation, average, and
|
|
* min/max cutoffs from the collected samples, then counts (and flags via
|
|
* $std_kills/$var_kills) how many samples fall outside the standard-deviation
|
|
* and variance thresholds.
|
|
*
|
|
* @param array<int, array<int, array<string, mixed>>> $rra
|
|
* @param array<int, array<int, array<int, float|int>>> $samples
|
|
*
|
|
* @return void
|
|
*/
|
|
function calculateOverallStatistics(&$rra, &$samples) {
|
|
global $percent, $stddev, $ds_min, $ds_max, $var_kills, $std_kills;
|
|
|
|
$rra_num = 0;
|
|
|
|
if (sizeof($rra)) {
|
|
foreach ($rra as $dses) {
|
|
$ds_num = 0;
|
|
|
|
if (sizeof($dses)) {
|
|
foreach ($dses as $ds) {
|
|
if (isset($samples[$rra_num][$ds_num])) {
|
|
$rra[$rra_num][$ds_num]['standard_deviation'] = standard_deviation($samples[$rra_num][$ds_num]);
|
|
|
|
if ($rra[$rra_num][$ds_num]['standard_deviation'] == 'NAN') {
|
|
$rra[$rra_num][$ds_num]['standard_deviation'] = 0;
|
|
}
|
|
$rra[$rra_num][$ds_num]['average'] = $rra[$rra_num][$ds_num]['sumofsamples'] / $rra[$rra_num][$ds_num]['numsamples'];
|
|
|
|
$rra[$rra_num][$ds_num]['min_cutoff'] = $rra[$rra_num][$ds_num]['average'] - ($stddev * $rra[$rra_num][$ds_num]['standard_deviation']);
|
|
|
|
if ($rra[$rra_num][$ds_num]['min_cutoff'] < $ds_min[$ds_num]) {
|
|
$rra[$rra_num][$ds_num]['min_cutoff'] = $ds_min[$ds_num];
|
|
}
|
|
|
|
$rra[$rra_num][$ds_num]['max_cutoff'] = $rra[$rra_num][$ds_num]['average'] + ($stddev * $rra[$rra_num][$ds_num]['standard_deviation']);
|
|
|
|
if ($rra[$rra_num][$ds_num]['max_cutoff'] > $ds_max[$ds_num]) {
|
|
$rra[$rra_num][$ds_num]['max_cutoff'] = $ds_max[$ds_num];
|
|
}
|
|
|
|
$rra[$rra_num][$ds_num]['numnksamples'] = 0;
|
|
$rra[$rra_num][$ds_num]['sumnksamples'] = 0;
|
|
$rra[$rra_num][$ds_num]['avgnksamples'] = 0;
|
|
|
|
// go through values and find cutoffs
|
|
$rra[$rra_num][$ds_num]['stddev_killed'] = 0;
|
|
$rra[$rra_num][$ds_num]['variance_killed'] = 0;
|
|
|
|
if (sizeof($samples[$rra_num][$ds_num])) {
|
|
foreach ($samples[$rra_num][$ds_num] as $sample) {
|
|
if (($sample > $rra[$rra_num][$ds_num]['max_cutoff']) ||
|
|
($sample < $rra[$rra_num][$ds_num]['min_cutoff'])) {
|
|
debug(sprintf("Std Kill: Value '%.4e', StandardDev '%.4e', StdDevLimit '%.4e'", $sample, $rra[$rra_num][$ds_num]['standard_deviation'], ($rra[$rra_num][$ds_num]['max_cutoff'] * (1 + $percent))));
|
|
$rra[$rra_num][$ds_num]['stddev_killed']++;
|
|
$std_kills = true;
|
|
} else {
|
|
$rra[$rra_num][$ds_num]['numnksamples']++;
|
|
$rra[$rra_num][$ds_num]['sumnksamples'] += $sample;
|
|
}
|
|
|
|
if ($rra[$rra_num][$ds_num]['variance_avg'] == 'NAN') {
|
|
// not enough samples to calculate
|
|
} elseif ($sample > ($rra[$rra_num][$ds_num]['variance_avg'] * (1 + $percent))) {
|
|
// kill based upon variance
|
|
debug(sprintf("Var Kill: Value '%.4e', VarianceDev '%.4e', VarianceLimit '%.4e'", $sample, $rra[$rra_num][$ds_num]['variance_avg'], ($rra[$rra_num][$ds_num]['variance_avg'] * (1 + $percent))));
|
|
$rra[$rra_num][$ds_num]['variance_killed']++;
|
|
$var_kills = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($rra[$rra_num][$ds_num]['numnksamples'] > 0) {
|
|
$rra[$rra_num][$ds_num]['avgnksamples'] = $rra[$rra_num][$ds_num]['sumnksamples'] / $rra[$rra_num][$ds_num]['numnksamples'];
|
|
}
|
|
} else {
|
|
$rra[$rra_num][$ds_num]['standard_deviation'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['average'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['min_cutoff'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['max_cutoff'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['numnksamples'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['sumnksamples'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['avgnksamples'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['stddev_killed'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['variance_killed'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['stddev_killed'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['numnksamples'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['sumnksamples'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['variance_killed'] = 'N/A';
|
|
$rra[$rra_num][$ds_num]['avgnksamples'] = 'N/A';
|
|
}
|
|
|
|
$ds_num++;
|
|
}
|
|
}
|
|
|
|
$rra_num++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prints a per-data-source summary table (plain text or HTML) of the computed
|
|
* statistics and kill counts for each RRA.
|
|
*
|
|
* @param array<int, array<int, array<string, mixed>>> $rra
|
|
*
|
|
* @return void
|
|
*/
|
|
function outputStatistics($rra) {
|
|
global $rra_cf, $rra_name, $ds_name, $rra_pdp, $html;
|
|
|
|
if (sizeof($rra)) {
|
|
if (!$html) {
|
|
print "\n";
|
|
printf("%10s %16s %10s %7s %7s %10s %10s %10s %10s %10s %10s %10s %10s %10s %10s\n",
|
|
'Size', 'DataSource', 'CF', 'Samples', 'NonNan', 'Avg', 'StdDev',
|
|
'MaxValue', 'MinValue', 'MaxStdDev', 'MinStdDev', 'StdKilled', 'VarKilled', 'StdDevAvg', 'VarAvg');
|
|
printf("%10s %16s %10s %7s %7s %10s %10s %10s %10s %10s %10s %10s %10s %10s %10s\n",
|
|
'----------', '---------------', '----------', '-------', '-------', '----------', '----------', '----------',
|
|
'----------', '----------', '----------', '----------', '----------', '----------',
|
|
'----------');
|
|
|
|
foreach ($rra as $rra_key => $dses) {
|
|
if (sizeof($dses)) {
|
|
foreach ($dses as $dskey => $ds) {
|
|
printf('%10s %16s %10s %7s %7s ' .
|
|
($ds['average'] < 1E6 ? '%10s ' : '%10.4e ') .
|
|
($ds['standard_deviation'] < 1E6 ? '%10s ' : '%10.4e ') .
|
|
(isset($ds['max_value']) ? ($ds['max_value'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') .
|
|
(isset($ds['min_value']) ? ($ds['min_value'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') .
|
|
(isset($ds['max_cutoff']) ? ($ds['max_cutoff'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') .
|
|
(isset($ds['min_cutoff']) ? ($ds['min_cutoff'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') .
|
|
'%10s %10s ' .
|
|
(isset($ds['avgnksampled']) ? ($ds['avgnksamples'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') .
|
|
(isset($ds['variance_avg']) ? ($ds['variance_avg'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') . "\n",
|
|
displayTime($rra_pdp[$rra_key]),
|
|
$ds_name[$dskey],
|
|
$rra_cf[$rra_key],
|
|
$ds['totalsamples'],
|
|
(isset($ds['numsamples']) ? $ds['numsamples'] : '0'),
|
|
($ds['average'] != 'N/A' ? round($ds['average'],2) : $ds['average']),
|
|
($ds['standard_deviation'] != 'N/A' ? round($ds['standard_deviation'],2) : $ds['standard_deviation']),
|
|
(isset($ds['max_value']) ? round($ds['max_value'],2) : 'N/A'),
|
|
(isset($ds['min_value']) ? round($ds['min_value'],2) : 'N/A'),
|
|
($ds['max_cutoff'] != 'N/A' ? round($ds['max_cutoff'],2) : $ds['max_cutoff']),
|
|
($ds['min_cutoff'] != 'N/A' ? round($ds['min_cutoff'],2) : $ds['min_cutoff']),
|
|
$ds['stddev_killed'],
|
|
$ds['variance_killed'],
|
|
($ds['avgnksamples'] != 'N/A' ? round($ds['avgnksamples'],2) : $ds['avgnksamples']),
|
|
(isset($ds['variance_avg']) ? round($ds['variance_avg'],2) : 'N/A'));
|
|
}
|
|
}
|
|
}
|
|
|
|
print "\n";
|
|
} else {
|
|
printf("<tr><th style='width:10%%;'>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th></tr>\n",
|
|
'Size', 'DataSource', 'CF', 'Samples', 'NonNan', 'Avg', 'StdDev',
|
|
'MaxValue', 'MinValue', 'MaxStdDev', 'MinStdDev', 'StdKilled', 'VarKilled', 'StdDevAvg', 'VarAvg');
|
|
|
|
foreach ($rra as $rra_key => $dses) {
|
|
if (sizeof($dses)) {
|
|
foreach ($dses as $dskey => $ds) {
|
|
printf('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>' .
|
|
($ds['average'] < 1E6 ? '%s</td><td>' : '%.4e</td><td>') .
|
|
($ds['standard_deviation'] < 1E6 ? '%s</td><td>' : '%.4e</td><td>') .
|
|
(isset($ds['max_value']) ? ($ds['max_value'] < 1E6 ? '%s</td><td>' : '%.4e</td><td>') : '%s</td><td>') .
|
|
(isset($ds['min_value']) ? ($ds['min_value'] < 1E6 ? '%s</td><td>' : '%.4e</td><td>') : '%s</td><td>') .
|
|
(isset($ds['max_cutoff']) ? ($ds['max_cutoff'] < 1E6 ? '%s</td><td>' : '%.4e</td><td>') : '%s</td><td>') .
|
|
(isset($ds['min_cutoff']) ? ($ds['min_cutoff'] < 1E6 ? '%s</td><td>' : '%.4e</td><td>') : '%s</td><td>') .
|
|
'%s</td><td>%s</td><td>' .
|
|
(isset($ds['avgnksampled']) ? ($ds['avgnksamples'] < 1E6 ? '%s</td><td>' : '%.4e</td><td>') : '%s</td><td>') .
|
|
(isset($ds['variance_avg']) ? ($ds['variance_avg'] < 1E6 ? "%s</td></tr>\n" : "%.4e</td></tr>\n") : "%s</td></tr>\n") . "\n",
|
|
displayTime($rra_pdp[$rra_key]),
|
|
$ds_name[$dskey],
|
|
$rra_cf[$rra_key],
|
|
$ds['totalsamples'],
|
|
(isset($ds['numsamples']) ? $ds['numsamples'] : '0'),
|
|
($ds['average'] != 'N/A' ? round($ds['average'],2) : $ds['average']),
|
|
($ds['standard_deviation'] != 'N/A' ? round($ds['standard_deviation'],2) : $ds['standard_deviation']),
|
|
(isset($ds['max_value']) ? round($ds['max_value'],2) : 'N/A'),
|
|
(isset($ds['min_value']) ? round($ds['min_value'],2) : 'N/A'),
|
|
($ds['max_cutoff'] != 'N/A' ? round($ds['max_cutoff'],2) : $ds['max_cutoff']),
|
|
($ds['min_cutoff'] != 'N/A' ? round($ds['min_cutoff'],2) : $ds['min_cutoff']),
|
|
$ds['stddev_killed'],
|
|
$ds['variance_killed'],
|
|
($ds['avgnksamples'] != 'N/A' ? round($ds['avgnksamples'],2) : $ds['avgnksamples']),
|
|
(isset($ds['variance_avg']) ? round($ds['variance_avg'],2) : 'N/A'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rewrites the dumped XML's <row> value lines, replacing any sample outside
|
|
* the computed thresholds with the RRA's average (or 'NaN'), up to $numspike
|
|
* replacements per RRA.
|
|
*
|
|
* @param array<int, string> $output
|
|
* @param array<int, array<int, array<string, mixed>>> $rra
|
|
*
|
|
* @return array<int, string>
|
|
*/
|
|
function updateXML(&$output, &$rra) {
|
|
global $numspike, $percent, $avgnan, $method, $total_kills;
|
|
|
|
// variance subroutine
|
|
$rra_num = 0;
|
|
$ds_num = 0;
|
|
$kills = 0;
|
|
$new_array = [];
|
|
|
|
if (sizeof($output)) {
|
|
foreach ($output as $line) {
|
|
if (substr_count($line, '<v>')) {
|
|
$linearray = explode('<v>', $line);
|
|
// discard the row
|
|
array_shift($linearray);
|
|
|
|
// initialize variables
|
|
$ds_num = 0;
|
|
$out_row = '<row>';
|
|
|
|
foreach ($linearray as $dsvalue) {
|
|
// peel off garbage
|
|
$dsvalue = trim(str_replace('</row>', '', str_replace('</v>', '', $dsvalue)));
|
|
|
|
if (strtolower($dsvalue) == 'nan') {
|
|
// do nothing, it's a NaN
|
|
} else {
|
|
if ($method == 2) {
|
|
if ($dsvalue > (1 + $percent) * $rra[$rra_num][$ds_num]['variance_avg']) {
|
|
if ($kills < $numspike) {
|
|
if ($avgnan == 'avg') {
|
|
$dsvalue = $rra[$rra_num][$ds_num]['variance_avg'];
|
|
} else {
|
|
$dsvalue = 'NaN';
|
|
}
|
|
$kills++;
|
|
$total_kills++;
|
|
}
|
|
}
|
|
} else {
|
|
if (($dsvalue > $rra[$rra_num][$ds_num]['max_cutoff']) ||
|
|
($dsvalue < $rra[$rra_num][$ds_num]['min_cutoff'])) {
|
|
if ($kills < $numspike) {
|
|
if ($avgnan == 'avg') {
|
|
$dsvalue = $rra[$rra_num][$ds_num]['average'];
|
|
} else {
|
|
$dsvalue = 'NaN';
|
|
}
|
|
$kills++;
|
|
$total_kills++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$out_row .= '<v> ' . $dsvalue . '</v>';
|
|
$ds_num++;
|
|
}
|
|
|
|
$out_row .= '</row>';
|
|
|
|
$new_array[] = $out_row;
|
|
} else {
|
|
if (substr_count($line, '</rra>')) {
|
|
$ds_minmax = [];
|
|
$rra_num++;
|
|
$kills = 0;
|
|
} elseif (substr_count($line, '</database>')) {
|
|
$ds_num++;
|
|
$kills = 0;
|
|
}
|
|
|
|
$new_array[] = $line;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $new_array;
|
|
}
|
|
|
|
/**
|
|
* Strips blank lines and `<!-- ... -->` XML comments from a dumped RRD file's
|
|
* lines.
|
|
*
|
|
* @param array<int, string> $output
|
|
*
|
|
* @return array<int, string>|null
|
|
*/
|
|
function removeComments(&$output) {
|
|
$new_array = [];
|
|
|
|
if (sizeof($output)) {
|
|
foreach ($output as $line) {
|
|
$line = trim($line);
|
|
|
|
if ($line == '') {
|
|
continue;
|
|
} else {
|
|
// is there a comment, remove it
|
|
$comment_start = strpos($line, '<!--');
|
|
|
|
if ($comment_start === false) {
|
|
// do nothing no line
|
|
} else {
|
|
$comment_end = strpos($line, '-->');
|
|
|
|
if ($comment_start == 0) {
|
|
$line = trim(substr($line, $comment_end + 3));
|
|
} else {
|
|
$line = trim(substr($line,0,$comment_start - 1) . substr($line,$comment_end + 3));
|
|
}
|
|
}
|
|
|
|
if ($line != '') {
|
|
$new_array[] = $line;
|
|
}
|
|
}
|
|
}
|
|
|
|
// transfer the new array back to the original array
|
|
return $new_array;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formats a PDP (primary data point) count as a human-readable duration
|
|
* (seconds, minutes, hours, or days) based on the RRD's step size.
|
|
*
|
|
* @param int $pdp
|
|
*
|
|
* @return string
|
|
*/
|
|
function displayTime($pdp) {
|
|
global $step;
|
|
|
|
$total_time = $pdp * $step; // seconds
|
|
|
|
if ($total_time < 60) {
|
|
return $total_time . ' secs';
|
|
} else {
|
|
$total_time = $total_time / 60;
|
|
|
|
if ($total_time < 60) {
|
|
return $total_time . ' mins';
|
|
} else {
|
|
$total_time = $total_time / 60;
|
|
|
|
if ($total_time < 24) {
|
|
return $total_time . ' hours';
|
|
} else {
|
|
$total_time = $total_time / 24;
|
|
|
|
return $total_time . ' days';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prints $string prefixed with "DEBUG: " when debug output is enabled.
|
|
*
|
|
* @param string $string
|
|
*
|
|
* @return void
|
|
*/
|
|
function debug($string) {
|
|
global $debug;
|
|
|
|
if ($debug) {
|
|
print 'DEBUG: ' . $string . "\n";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computes the (population) standard deviation of a set of numeric samples.
|
|
*
|
|
* @param array<int, float|int> $samples
|
|
*
|
|
* @return float
|
|
*/
|
|
function standard_deviation($samples) {
|
|
$sample_count = count($samples);
|
|
$sample_square = [];
|
|
|
|
for ($current_sample = 0; $sample_count > $current_sample; ++$current_sample) {
|
|
$sample_square[$current_sample] = pow($samples[$current_sample], 2);
|
|
}
|
|
|
|
return sqrt(array_sum($sample_square) / $sample_count - pow((array_sum($samples) / $sample_count), 2));
|
|
}
|
|
|
|
// display_help - displays the usage of the function
|
|
/**
|
|
* Prints removespikes' version banner and command-line usage/help text.
|
|
*
|
|
* @return void
|
|
*/
|
|
function display_help() {
|
|
global $using_cacti;
|
|
|
|
if ($using_cacti) {
|
|
$version = spikekill_version();
|
|
} else {
|
|
$version = 'v1.1';
|
|
}
|
|
|
|
print 'Cacti Spike Remover ' . ($using_cacti ? 'v' . $version['version'] : $version) . ", Copyright 2004-2026, The Cacti Group, Inc.\n\n";
|
|
print "Usage:\n";
|
|
print "removespikes.php -R|--rrdfile=rrdfile [-M|--method=stddev] [-A|--avgnan] [-S|--stddev=N]\n";
|
|
print " [-P|--percent=N] [-N|--number=N] [-D|--dryrun] [-d|--debug]\n";
|
|
print " [--html] [-h|--help|-v|-V|--version]\n\n";
|
|
|
|
print "The RRDfile input parameter is mandatory. If no other input parameters are specified the defaults\n";
|
|
print "are taken from the Spikekill Plugin settings.\n\n";
|
|
|
|
print "-M|--method - The spike removal method to use. Options are 'stddev'|'variance'\n";
|
|
print "-A|--avgnan - The spike replacement method to use. Options are 'avg'|'nan'\n";
|
|
print "-S|--stddev - The number of standard deviations +/- allowed\n";
|
|
print "-P|--percent - The sample to sample percentage variation allowed\n";
|
|
print "-N|--number - The maximum number of spikes to remove from the RRDfile\n";
|
|
print "-D|--dryrun - If specified, the RRDfile will not be changed. Instead a summary of\n";
|
|
print " changes that would have been performed will be issued.\n";
|
|
print "--backup - Backup the original RRDfile to preserve prior values.\n\n";
|
|
|
|
print "The remainder of arguments are informational\n";
|
|
print "--html - Format the output for a web browser\n";
|
|
print "-d|--debug - Display verbose output during execution\n";
|
|
print "-v -V --version - Display this help message\n";
|
|
print "-h --help - display this help message\n";
|
|
}
|