mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-25 04:27:56 +09:00
* Detect and repair RRDfile Data Source count mismatches via RRDtune Report when a Graph requires more or fewer Data Sources than its RRDfile contains, generate the rrdtool tune DS:/DEL: command to correct it, and run it when the RRDfile is writable. Applies to data_debug (rrdtool_cacti_compare) and the RRDchecker batch process (do_rrdcheck). The decision logic lives in pure helpers (rrdtool_cacti_compare_ds, rrdcheck_ds_findings) with unit tests. Resolves #8366 * rrdcheck: only probe local writability for local storage Under the storage proxy the RRDfile is remote, so is_resource_writable() checks the wrong filesystem. Skip the local probe when using the proxy and let it enforce remote permissions and report failures via its response. * rrdcheck: validate tune args, enforce RRA containment, fix failure detection Validate every tune specification against RRDtool's documented DS grammar (ds-name 1-19 [a-zA-Z0-9_], known DST, integer heartbeat, numeric-or-U min/max) and escape it before use. For local storage require the RRDfile to resolve inside the RRA directory via data_source_path_within_rra(). Detect failures from the real return value (false/null/empty or missing OK) instead of casting to string, so a failed tune is no longer logged as success. * rrd: normalize a 0/0 data source range to 0:U when building a tune add spec rrdtool_function_info_from_ds() passes min/max through unchanged, so a source stored as 0/0 produced DS:name:type:hb:0:0 which RRDtool rejects. Mirror RRD creation and emit 0:U so the repair paths can restore those sources.
162 lines
6.1 KiB
PHP
162 lines
6.1 KiB
PHP
<?php
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
require_once dirname(__DIR__) . '/Helpers/UnitStubs.php';
|
|
require_once CACTI_PATH_LIBRARY . '/rrd.php';
|
|
require_once CACTI_PATH_LIBRARY . '/rrdcheck.php';
|
|
|
|
test('rrdcheck_ds_findings reports an extra RRDfile data source with a DEL spec', function () {
|
|
$findings = rrdcheck_ds_findings(['in' => 'in'], ['in' => [], 'legacy' => []], [], '/rra/x.rrd');
|
|
|
|
expect($findings[0]['message'])->toContain('includes 2');
|
|
|
|
$del = array_values(array_filter($findings, fn($f) => $f['spec'] === 'DEL:legacy'));
|
|
expect($del)->toHaveCount(1);
|
|
expect($del[0]['message'])->toContain('no longer requires');
|
|
});
|
|
|
|
test('rrdcheck_ds_findings reports a missing data source with a DS add spec', function () {
|
|
$cacti_ds = ['out' => ['type' => 'COUNTER', 'minimal_heartbeat' => 600, 'min' => 0, 'max' => 'U']];
|
|
$findings = rrdcheck_ds_findings(['in' => 'in', 'out' => 'out'], ['in' => []], $cacti_ds, '/rra/x.rrd');
|
|
|
|
expect($findings[0]['message'])->toContain('only includes 1');
|
|
|
|
$add = array_values(array_filter($findings, fn($f) => $f['spec'] === 'DS:out:COUNTER:600:0:U'));
|
|
expect($add)->toHaveCount(1);
|
|
expect($add[0]['message'])->toContain('missing from the RRDfile');
|
|
});
|
|
|
|
test('rrdcheck_ds_findings falls back to a plain message when no Cacti definition exists', function () {
|
|
$findings = rrdcheck_ds_findings(['out' => 'out'], [], [], '/rra/x.rrd');
|
|
|
|
$miss = array_values(array_filter($findings, fn($f) => str_contains($f['message'], 'exists in the Database')));
|
|
expect($miss)->toHaveCount(1);
|
|
expect($miss[0]['spec'])->toBe('');
|
|
});
|
|
|
|
test('rrdcheck_ds_findings flags an empty data source name', function () {
|
|
$findings = rrdcheck_ds_findings(['' => ''], [], [], '/rra/x.rrd');
|
|
|
|
$empty = array_values(array_filter($findings, fn($f) => str_contains($f['message'], 'is empty')));
|
|
expect($empty)->toHaveCount(1);
|
|
expect($empty[0]['spec'])->toBe('');
|
|
});
|
|
|
|
test('rrdcheck_ds_findings is quiet for matching names with equal counts', function () {
|
|
$findings = rrdcheck_ds_findings(['keep' => 'keep', 'a' => 'a'], ['keep' => [], 'b' => []], [], '/rra/x.rrd');
|
|
|
|
$counts = array_values(array_filter($findings, fn($f) => str_contains($f['message'], 'Graph requires')));
|
|
expect($counts)->toHaveCount(0);
|
|
|
|
$del = array_values(array_filter($findings, fn($f) => $f['spec'] === 'DEL:b'));
|
|
expect($del)->toHaveCount(1);
|
|
|
|
$miss = array_values(array_filter($findings, fn($f) => str_contains($f['message'], "'a' exists in the Database")));
|
|
expect($miss)->toHaveCount(1);
|
|
});
|
|
|
|
test('rrdcheck_valid_tune_spec accepts well-formed DS and DEL specifications', function () {
|
|
expect(rrdcheck_valid_tune_spec('DEL:traffic_in'))->toBeTrue();
|
|
expect(rrdcheck_valid_tune_spec('DS:traffic_in:COUNTER:600:0:U'))->toBeTrue();
|
|
expect(rrdcheck_valid_tune_spec('DS:x:GAUGE:300:-5:1.5'))->toBeTrue();
|
|
});
|
|
|
|
test('rrdcheck_valid_tune_spec rejects malformed or unsafe specifications', function () {
|
|
expect(rrdcheck_valid_tune_spec('DEL:bad name'))->toBeFalse();
|
|
expect(rrdcheck_valid_tune_spec('DEL:in; rm -rf /'))->toBeFalse();
|
|
expect(rrdcheck_valid_tune_spec('DS:in:BOGUS:600:0:U'))->toBeFalse();
|
|
expect(rrdcheck_valid_tune_spec('DS:toolongdatasourcename0:COUNTER:600:0:U'))->toBeFalse();
|
|
expect(rrdcheck_valid_tune_spec('DS:in:COUNTER:x:0:U'))->toBeFalse();
|
|
});
|
|
|
|
test('rrdcheck_tune_rrdfile refuses a malformed tune specification', function () {
|
|
$pipes = [];
|
|
$result = rrdcheck_tune_rrdfile(CACTI_PATH_RRA . '/x.rrd', 'DEL:in;rm', 1, false, $pipes);
|
|
|
|
expect($result)->toContain('not valid');
|
|
});
|
|
|
|
test('rrdcheck_tune_rrdfile refuses a local RRDfile outside the RRA directory', function () {
|
|
$pipes = [];
|
|
$path = sys_get_temp_dir() . '/cacti_outside_' . uniqid() . '.rrd';
|
|
$result = rrdcheck_tune_rrdfile($path, 'DEL:in', 1, false, $pipes);
|
|
|
|
expect($result)->toContain('outside the RRA directory');
|
|
});
|
|
|
|
test('rrdcheck_tune_rrdfile skips a tune when the RRDfile is not writable', function () {
|
|
$pipes = [];
|
|
$path = CACTI_PATH_RRA . '/missing_' . uniqid() . '/x.rrd';
|
|
$result = rrdcheck_tune_rrdfile($path, 'DEL:in', 1, false, $pipes);
|
|
|
|
expect($result)->toContain('not writable');
|
|
});
|
|
|
|
test('rrdcheck_tune_rrdfile runs the tune over the pipe and reports success', function () {
|
|
$file = CACTI_PATH_RRA . '/ok_' . uniqid() . '.rrd';
|
|
touch($file);
|
|
|
|
$stdin = fopen('php://temp', 'r+');
|
|
$stdout = fopen('php://temp', 'r+');
|
|
|
|
fwrite($stdout, "OK\n");
|
|
rewind($stdout);
|
|
|
|
$pipes = [$stdin, $stdout];
|
|
$result = rrdcheck_tune_rrdfile($file, 'DEL:in', 1, false, $pipes);
|
|
|
|
fclose($stdin);
|
|
fclose($stdout);
|
|
@unlink($file);
|
|
|
|
expect($result)->toContain('run automatically');
|
|
});
|
|
|
|
test('rrdcheck_tune_rrdfile treats a missing OK acknowledgement as a failure', function () {
|
|
$file = CACTI_PATH_RRA . '/noack_' . uniqid() . '.rrd';
|
|
touch($file);
|
|
|
|
$stdin = fopen('php://temp', 'r+');
|
|
$stdout = fopen('php://temp', 'r+');
|
|
|
|
$pipes = [$stdin, $stdout];
|
|
$result = rrdcheck_tune_rrdfile($file, 'DEL:in', 1, false, $pipes);
|
|
|
|
fclose($stdin);
|
|
fclose($stdout);
|
|
@unlink($file);
|
|
|
|
expect($result)->toContain('failed');
|
|
});
|
|
|
|
test('rrdcheck_tune_rrdfile reports a failing tune', function () {
|
|
$file = CACTI_PATH_RRA . '/fail_' . uniqid() . '.rrd';
|
|
touch($file);
|
|
|
|
$stdin = fopen('php://temp', 'r+');
|
|
$stdout = fopen('php://temp', 'r+');
|
|
|
|
fwrite($stdout, "ERROR: boom\n");
|
|
rewind($stdout);
|
|
|
|
$pipes = [$stdin, $stdout];
|
|
$result = rrdcheck_tune_rrdfile($file, 'DEL:in', 1, false, $pipes);
|
|
|
|
fclose($stdin);
|
|
fclose($stdout);
|
|
@unlink($file);
|
|
|
|
expect($result)->toContain('failed');
|
|
});
|