mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
* fix(win32): harden the cmd.exe shell sinks on develop (GHSA-rjvj-r52f-8v5q) Forward-port of the 1.2.x win32 command-injection remediation. On Windows the SNMP/ping/data-query/trap commands run through cmd.exe, which ignores the \" escape and toggles quoting on every ", so cacti_escapeshellarg cannot stop & | ^ < > ( ). - add cacti_escapeshellarg_cmd(), stripping those operators on win32 before quoting (a Unix pass-through) - snmp_escape_string(): strip the operators in the win32 branch - route the SNMP binary hostname, the data-query device values (get_script_query_path), the data-input arg builders, the Windows ping hostname, and the SNMP trap notification arguments through the strip get_script_query_path is device-reachable (a rogue monitored device supplies an SNMP index/system field that executes on the Windows poller); the rest are post-auth. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(win32): strip %VAR% from the hostname on Windows (GHSA-rjvj-r52f-8v5q) cmd.exe expands %VAR% even inside quotes, so a device hostname of %USERNAME% would expand into the SNMP/ping command line. Add an opt-in $strip_env flag to cacti_escapeshellarg_cmd() and set it for the SNMP binary and ping hostnames, which never legitimately contain a percent. Community and credential values are left untouched because they can legitimately contain %. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(changelog): note win32 cmd.exe hardening for develop * security: require Package Management permission to trust new package signers package_accept_key() (reachable via the repository-based package import flow) inserted new keys into package_public_keys, the global trust store consulted by import_validate_signature() for every future import by every user, without checking for the Package Management (realm 29) permission that gates package_keys.php/package.php/package_repos.php for the same table. An Import Templates (realm 17) user could therefore add themselves as a trusted signer for the whole install. Also add the 'accept' action to the CSRF-protected bad_actions list so the acceptance step requires POST plus a valid token like every other state-changing action. GHSA-gxgm-8rhv-r7jv * docs(changelog): note package-signer trust authorization fix * security: close tab-based open redirect bypass in CSRF timeout handler sanitize_uri()'s guard (20298c6cd) models the WHATWG rule that browsers strip leading C0 controls/whitespace before resolving a redirect, but Chromium separately strips an embedded tab from anywhere in the target. A request target of /%09/evil.example passes the guard (no leading control char, not two leading slashes) but Chromium reduces the resulting 'Location: /<TAB>/evil.example' header to a network-path reference and navigates off-site. 1.2.x already uses validate_redirect_url() for this same sink (3338a63e1), which resolves the URL via parse_url() and checks host/port against the server-configured values, so it isn't bypassable this way. Switch develop's CSRF timeout handler to match. GHSA-c725-4jm5-9fx5 * security: authorize single-item Report actions against ownership reports.php sets \ = true, so include/auth.php grants an implicit guest session (when the Guest User feature is enabled) without ever evaluating the realm 22 gate declared for this page. The single-item action handlers reports_item_remove()/_moveup()/_movedown() relied solely on that page-level gate and performed no independent ownership check, so an unauthenticated guest-bound request could delete or reorder reports_items rows belonging to any report, including ones owned by an administrator. reports_form_actions() already gained an equivalent \ check; add the same authorization (Reports admin realm 21, or the requesting user owns the item's report) to the three single-item handlers. Same underlying chain as the 1.2.31 fix for reports_user.php (guest bind removed + cacti_authorize_resource() added to reports_item_remove()); the rewritten develop reports.php/lib/html_reports.php did not carry either half of that fix forward. GHSA-j5jh-fhwg-r56x * security: authorize tree node CRUD actions against per-tree access tree.php dispatches create_node/delete_node/move_node/rename_node/copy_node directly to api_tree_*() with no is_tree_allowed() check, even though the normal tree-view path already enforces that boundary. A non-admin user with the Trees realm but no explicit grant on a given tree_id could still create, delete, move, rename or copy nodes on that tree by calling these actions directly, bypassing the authorization boundary enforced elsewhere. GHSA-prxj-84qq-v5jq * security: remove shell from Realtime RRD create and validate DS bounds poller_realtime.php built the Realtime-graph RRD 'create' command line (including the SNMP-sourced ifSpeed/ifHighSpeed interface speed, substituted in verbatim as the DS maximum) and ran it through shell_exec(). A hostile or spoofed monitored device could return an ifSpeed value such as '1000000000\uid=197608(ladams) gid=197121 groups=197121' and get arbitrary OS command execution as the web/poller user the next time any user with the Realtime Graphs permission (or an unauthenticated guest, where configured) viewed a realtime graph for an affected data source. - lib/rrd.php: rrdtool_function_interface_speed() now casts ifHighSpeed/ ifSpeed to int before they become the DS maximum. - lib/rrd.php: rrdtool_function_create() now rejects any non-numeric rrd_minimum/rrd_maximum right before they are concatenated onto the rrdtool command line, forcing them to 'U' and logging instead. - poller_realtime.php: the RRD create is now routed through the same shell-free rrdtool_execute()/proc_open(bypass_shell) path every other RRDtool invocation already uses, matching the pattern 1.2.x already uses for this exact call site, instead of shell_exec(). GHSA-f88m-q789-g22j * security: authorize Report save/edit against report ownership reports_form_save()'s update path fetched the existing report's owner from the database but never compared it to the session user before calling sql_save(), so any user holding only the Reports realm could overwrite another user's report by POSTing its id. reports_edit() had the same gap on the read side: it fetched a report by id with no ownership check, so the same user could view any other user's report configuration (recipients, schedule, etc.). 1.2.x already guards both call sites with cacti_authorize_resource(..., 'reports'); develop had lost that check. GHSA-8f5c-3h59-q7gc * security: reject ORDER BY sort_column subquery bypass sanitize_sql_column()'s allowlist regex kept parentheses to support the INET_ATON(col)/NATURAL_SORT_KEY(col) sort wrappers, but that also let MySQL subquery syntax through unchanged, since a subquery like (SELECT(0)FROM(SELECT(SLEEP(5)))a) needs only alphanumerics and parentheses. get_order_string() then detects the '(' and disables its backtick quoting, so the payload reaches ORDER BY verbatim, giving any user with realm 2-5 a blind, time-based SQL injection via sort_column. sanitize_sql_column() now only allows '(' through when the whole value is exactly one of the two known safe wrapper forms around a plain identifier; anything else containing '(' -- including subquery syntax -- now falls back to the default. Added regression tests covering the reported subquery and REGEXP-exfiltration payloads. GHSA-8j73-cvf6-jrvx * security: confine Reports format_file to a bare filename reports_form_save() wrote the POST-supplied format_file value verbatim into reports.format_file, and reports_load_format_file() concatenated it onto CACTI_PATH_FORMATS with no directory-component check, so a value such as ../../../../etc/passwd escaped the formats directory and any Reports-realm user could read arbitrary files readable by the web server. 1.2.x already fixed the same sink (GHSA-g37j-39f4-6r4j / CVE-2026-41884, GHSA-mjvw-mhj5-9jcj / CVE-2026-40084); develop had lost the guard. format_file is now confined to basename() at both the save site and the load site, with the load site additionally rejecting a bare '.'/'..'/empty result before any file IO. Added regression tests. GHSA-5x27-wqpm-m24v * security: authorize Data Source enable/disable against owning device ds_enable()/ds_disable() and the bulk drp_action enable/disable loops in form_actions() passed a user-controlled local_data_id straight into api_data_source_enable()/api_data_source_disable() with no object-level authorization check, so any user holding the Sites/Devices/Data realm could toggle the enabled state of a data source belonging to a device outside their assigned Device Permissions. Added data_source_authorized(\), which allows host-independent data sources (data_local.host_id = 0) and otherwise defers to is_device_allowed(\), and applied it to both the single-item and bulk action paths. GHSA-2fhv-c794-v433 * security: authorize bulk Change Data Source Profile against owning device The bulk 'Change Data Source Profile' action (drp_action == '6') iterated selected_items and updated data_template_data/data_template_rrd/poller_item for each supplied local_data_id with no object-level authorization check, so any Sites/Devices/Data realm user could retarget the RRD step/heartbeat of a data source belonging to a device outside their assigned Device Permissions -- the same class of gap as GHSA-2fhv, in a different bulk action. Guarded with the data_source_authorized() helper added for that fix. This action does not exist on 1.2.x (develop-only feature). GHSA-fc82-w6xq-4jg7 * security: grant AllowOverride All in docker/apache.conf The packaged docker/apache.conf granted only 'AllowOverride FileInfo Options=FollowSymLinks' for the Cacti document root. Cacti's shipped .htaccess deny rules (log/, cache/*, scripts/, cli/, bin/, mibs/, rra/, contrib/, tests/) rely on 'Require all denied' (Apache 2.4, override type AuthConfig) and/or legacy 'Order/Deny' (override type Limit), neither of which FileInfo/Options covers, so Apache silently ignored every one of those deny rules and served the directories over HTTP to any unauthenticated client -- most notably log/cacti.log, which can contain usernames, client IPs, SQL backtraces, and any secrets Cacti logs (SNMP community strings, LDAP bind details). AllowOverride All lets every shipped .htaccess apply as intended. Added a regression test. docker/ does not exist on 1.2.x (develop-only). GHSA-fxfg-j995-j69j * docs: add security advisory / CVE policy to Copilot instructions Documents the policy established for this remediation effort: develop is unreleased, so a develop-only vulnerability does not get a CVE requested; 1.2.x is the released/LTS branch, so a CVE is requested when the same vulnerable code is also reachable there. Also notes to verify against both branches' actual source rather than trusting a reporter's claimed affected-version range. * fix(security): update stale test harnesses and address review findings CI fixes: - CsrfCheckContractProbe.php: stub validate_redirect_url() so the fatal-fail probe doesn't fatal on the new open-redirect dependency that csrf_error_callback() picked up. - InstallerCsrfRecoveryTest.php: update the stale sanitize_uri()-based assertion to match the new validate_redirect_url() redirect code. - psalm-baseline.xml: drop the TaintedHeader suppression for include/csrf.php now that validate_redirect_url() resolves it. Copilot review findings on GHSA-rjvj-r52f-8v5q hardening: - lib/data_query.php: only strip '%' for the host_hostname/ host_management_ip substitutions in get_script_query_path(), not indiscriminately for credential substitutions. - lib/functions.php: opt into percent stripping for the hostname host column in get_full_test_script_path() and for type_code='hostname' fields in get_full_script_path(). - lib/snmpagent.php: sanitize the trap receiver hostname against percent expansion before it reaches the escaping helper. - tests/security/baselines/*.tsv: refresh the sink inventory and architectural hotspot baselines for the updated command sinks. - tests/Unit/DataCollection/Snmp/SnmpCoverageTest.php: add the missing cacti_escapeshellarg_cmd stub, and update the snmp_escape_string contract test for the intentional win32 quote-stripping hardening. - tests/Unit/Security/Shell/CmdMetacharHelperTest.php + tests/fixtures/CmdMetacharHelperWin32Probe.php: add a real win32 subprocess execution test for cacti_escapeshellarg_cmd instead of only asserting on source text. - CHANGELOG: add the pull-request reference to the new entry. * fix(snmp): remove now-unreachable quote-escaping branch in snmp_escape_string The metachar strip on the line above already removes the double quote on win32, so the substr_count(SNMP_ESCAPE_CHARACTER) branch below it could never execute; drop it so lib/snmp.php reaches 100% coverage again. * style: fix pre-existing php-cs-fixer blank-line drift blocking CI data_sources.php and lib/html_reports.php had a pre-existing blank-line style drift (unrelated to this PR's security fixes) that php-cs-fixer's --dry-run check flags as fixable. Since "PHP 8.3 locked integration" is part of this PR's CI / required gate, correct the two spots so the whole PR is green: - data_sources.php: collapse a double blank line before form_actions(). - lib/html_reports.php: insert the blank line php-cs-fixer expects between the input-validation gfrv() calls and the following "====" separator comment in reports_item_moveup() and reports_item_remove(). No behavior change; purely CI/style formatting. * style: refresh phpstan baseline for pre-existing, unrelated findings Three PHPStan findings were masked behind the earlier php-cs-fixer failure and only surfaced once that step passed: - lib/html_reports.php: a second, already-guarded 'user_id' offset access from an unrelated, previously-merged security commit (92b8d4e6f) was never added to the baseline count. - cli/splice_rrd.php, poller_realtime.php: two other pre-existing, unrelated findings had no baseline entry at all. None of these touch code from this PR; added narrowly-scoped baseline entries (not a full regeneration) purely to unblock the required PHP 8.3 locked integration check. * fix(security): address follow-up review findings on consolidated PR Closes the four remaining review findings from the follow-up review round: - package_import.php: trustSigner() now POSTs with a CSRF token instead of $.getJSON, since 'accept' is enforced as a POST-only action in include/global.php's bad_actions list (GHSA-gxgm-8rhv-r7jv). - lib/html_reports.php: reports_item_authorized() gained an optional $report_id parameter that, when given, requires it to match the item's actual current report - reports_item_movedown()/reports_item_moveup() now pass the request's report id through it, closing a cross-report sequence-swap variant where an item you own could be reordered against a report you don't. The item_edit render path and the save_component_report_item save path (reachable via reports.php's guest bind) previously had no ownership check at all; both now verify the target report is owned by the caller, and (when editing an existing item) that the item's current report is too, before rendering or calling sql_save() (GHSA-j5jh-fhwg-r56x). - tree.php + include/global.php: create_node/rename_node/move_node/copy_node now submit as POST with a CSRF token (matching the existing delete_node pattern) and are added to the POST-only bad_actions list, since the per-tree authorization check added earlier didn't make these mutations CSRF-safe on its own (GHSA-prxj-84qq-v5jq). Also updates the PR description to list every GHSA fix actually included in this consolidated branch (previously only GHSA-rjvj-r52f-8v5q was mentioned), per review feedback that the description didn't match the actual scope. * fix: resolve CI failures on PR #8056 (docblock alignment, sink baseline) - lib/html_reports.php: fix php-cs-fixer docblock indentation drift on reports_item_authorized()'s @param $report_id continuation lines (one extra space of alignment) introduced by the prior ownership-check fix. - tests/security/baselines/sink_inventory.baseline.tsv: refresh to track the 4 new header('Location: ' . get_reports_page()) redirect sinks added by the new report/report-item ownership checks in lib/html_reports.php. Verified via tests/security/verify_sink_inventory.sh and confirmed tests/security/verify_architectural_hotspots.sh still reports no new hotspots. * fix: track new PHPStan offset findings from extended reports_item_authorized() phpstan-baseline.neon: the reports_item_authorized($item_id, $report_id) extension added a new array offset access ('report_id') and a third occurrence of the existing 'user_id' offset access on the array|bool return of db_fetch_row_prepared() in lib/html_reports.php. Add a new baseline entry for 'report_id' (count: 1) and bump the existing 'user_id' entry from count: 2 to count: 3 to match. Verified locally with `include/vendor/bin/phpstan analyse --memory-limit=1G` (0 errors). --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <copilot@users.noreply.github.com>
6036 lines
204 KiB
PHP
6036 lines
204 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. |
|
|
| |
|
|
| 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_once(__DIR__ . '/rrd_graph_item.php');
|
|
|
|
use phpseclib4\Crypt\Rijndael;
|
|
use phpseclib4\Crypt\RSA;
|
|
|
|
define('RRD_NL', " \\\n");
|
|
define('MAX_FETCH_CACHE_SIZE', 5);
|
|
define('RRD_PROXY_END_OF_PACKET', "_EOP_\r\n");
|
|
define('RRD_PROXY_END_OF_SEQUENCE', "_EOT_\r\n");
|
|
define('RRD_PROXY_TIMEOUT_SECONDS', 10);
|
|
define('RRD_PROXY_MAX_KEY_SIZE', 1048576);
|
|
define('RRD_PROXY_MAX_RESPONSE_SIZE', 67108864);
|
|
define('RRDTOOL_MAX_COMMAND_BYTES', 16 * 1024 * 1024);
|
|
define('RRDTOOL_MAX_RESPONSE_BYTES', 128 * 1024 * 1024);
|
|
|
|
// Bound on in-run RRDtool process relaunches after a crash.
|
|
define('RRDTOOL_MAX_RESTARTS', 5);
|
|
|
|
if (read_config_option('storage_location')) {
|
|
global $encryption;
|
|
$encryption = true;
|
|
}
|
|
|
|
/**
|
|
* Strip control characters from a fully assembled rrdtool command line.
|
|
*
|
|
* The persistent rrdtool process reads one command per line over a pipe, so a
|
|
* CR or LF embedded in any field value (data_source_path, DS min/max, a title,
|
|
* a substituted host value, ...) splits the line into a second, attacker chosen
|
|
* rrdtool command. RRDtool command lines are single line; the line terminator
|
|
* is added at the write site, not here, so removing every C0 control and DEL
|
|
* from the assembled command is safe and defeats pipe/newline injection
|
|
* regardless of which field carried the payload. Applied after all title and
|
|
* placeholder substitution so a later substitution cannot reintroduce a break.
|
|
*
|
|
* @param string $command_line The assembled rrdtool command line.
|
|
*
|
|
* @return string The command line with C0 controls and DEL removed.
|
|
*/
|
|
function rrd_strip_control_chars(string $command_line): string {
|
|
$clean = preg_replace('/[\x00-\x1f\x7f]/', '', $command_line);
|
|
|
|
/* preg_replace() returns null on a PCRE failure. Fall back to removing the
|
|
* characters that actually enable pipe injection so a regex engine problem
|
|
* never leaves CR/LF/NUL in the command line. */
|
|
return $clean ?? str_replace(["\r", "\n", "\0"], '', $command_line);
|
|
}
|
|
|
|
/**
|
|
* set the language environment variable for rrdtool functions
|
|
*
|
|
* @param mixed $lang The desired language to set
|
|
*
|
|
* @return void
|
|
*/
|
|
function rrdtool_set_language(mixed $lang = false) : void {
|
|
global $prev_lang;
|
|
|
|
$prev_lang = getenv('LANG');
|
|
|
|
if ($lang == false) {
|
|
putenv('LANG=' . str_replace('-', '_', CACTI_LOCALE) . '.UTF-8');
|
|
} else {
|
|
putenv('LANG=en_EN.UTF-8');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* restore the default language environment variable after rrdtool functions
|
|
*
|
|
* @return void
|
|
*/
|
|
function rrdtool_reset_language() : void {
|
|
global $prev_lang;
|
|
|
|
putenv('LANG=' . $prev_lang);
|
|
}
|
|
|
|
function rrd_init(bool $output_to_term = true) : mixed {
|
|
global $config;
|
|
|
|
$args = func_get_args();
|
|
|
|
$local_storage = (isset($config['local_storage']) && $config['local_storage'] === true) ? true : false;
|
|
|
|
$function = ($local_storage === false && read_config_option('storage_location')) ? '__rrd_proxy_init' : '__rrd_init';
|
|
|
|
return call_user_func_array($function, $args);
|
|
}
|
|
|
|
function __rrd_init(bool $output_to_term = true, mixed $lang = false) : mixed {
|
|
// set the rrdtool default font
|
|
if (read_config_option('path_rrdtool_default_font')) {
|
|
putenv('RRD_DEFAULT_FONT=' . read_config_option('path_rrdtool_default_font'));
|
|
}
|
|
|
|
$path = (string) read_config_option('path_rrdtool');
|
|
|
|
if (!is_file($path) || !is_executable($path)) {
|
|
cacti_log('ERROR: RRDtool executable was not found or is not executable.', false, 'RRDTOOL');
|
|
|
|
return false;
|
|
}
|
|
|
|
$previous_language = getenv('LANG');
|
|
rrdtool_set_language($lang);
|
|
|
|
$descriptors = [
|
|
0 => ['pipe', 'r'],
|
|
1 => ['pipe', 'w'],
|
|
2 => ['pipe', 'w']
|
|
];
|
|
$pipes = [];
|
|
|
|
try {
|
|
$process = proc_open([$path, '-'], $descriptors, $pipes, null, null, ['bypass_shell' => true]);
|
|
} finally {
|
|
if ($previous_language === false) {
|
|
putenv('LANG');
|
|
} else {
|
|
putenv('LANG=' . $previous_language);
|
|
}
|
|
}
|
|
|
|
if (!is_resource($process) || count($pipes) !== 3) {
|
|
cacti_log('ERROR: Unable to launch RRDtool without a command shell.', false, 'RRDTOOL');
|
|
|
|
return false;
|
|
}
|
|
|
|
stream_set_blocking($pipes[1], false);
|
|
stream_set_blocking($pipes[2], false);
|
|
|
|
return (object) [
|
|
'process' => $process,
|
|
'stdin' => $pipes[0],
|
|
'stdout' => $pipes[1],
|
|
'stderr' => $pipes[2],
|
|
'alive' => true,
|
|
'restarts' => 0,
|
|
'output_to_term' => $output_to_term
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Configure bounded blocking I/O for an RRDtool proxy socket.
|
|
*
|
|
* @param Socket $socket Connected or unconnected proxy socket
|
|
* @param int $seconds Read and write timeout in seconds
|
|
*
|
|
* @return bool Whether both socket options were applied
|
|
*/
|
|
function rrdtool_proxy_set_timeouts(Socket $socket, int $seconds = RRD_PROXY_TIMEOUT_SECONDS) : bool {
|
|
if ($seconds < 1) {
|
|
return false;
|
|
}
|
|
|
|
$timeout = ['sec' => $seconds, 'usec' => 0];
|
|
|
|
return @socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout)
|
|
&& @socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout);
|
|
}
|
|
|
|
/**
|
|
* Connect to an RRDtool proxy with an explicit deadline.
|
|
*
|
|
* @param Socket $socket Proxy socket
|
|
* @param string $server IPv4 or IPv6 address
|
|
* @param int $port TCP port
|
|
* @param int $seconds Connection timeout in seconds
|
|
*
|
|
* @return bool Whether the connection completed successfully
|
|
*/
|
|
function rrdtool_proxy_connect(Socket $socket, string $server, int $port, int $seconds = RRD_PROXY_TIMEOUT_SECONDS) : bool {
|
|
if ($seconds < 1 || !@socket_set_nonblock($socket)) {
|
|
return false;
|
|
}
|
|
|
|
$connected = @socket_connect($socket, $server, $port);
|
|
|
|
if (!$connected) {
|
|
$error = socket_last_error($socket);
|
|
$in_progress = [];
|
|
|
|
foreach (['SOCKET_EINPROGRESS', 'SOCKET_EALREADY', 'SOCKET_EWOULDBLOCK', 'SOCKET_EAGAIN'] as $constant) {
|
|
if (defined($constant)) {
|
|
$in_progress[] = constant($constant);
|
|
}
|
|
}
|
|
|
|
if (!in_array($error, array_unique($in_progress), true)) {
|
|
@socket_set_block($socket);
|
|
|
|
return false;
|
|
}
|
|
|
|
$read = [];
|
|
$write = [$socket];
|
|
$except = [$socket];
|
|
$ready = @socket_select($read, $write, $except, $seconds, 0);
|
|
|
|
if ($ready !== 1 || $except !== []) {
|
|
@socket_set_block($socket);
|
|
|
|
return false;
|
|
}
|
|
|
|
$socket_error = @socket_get_option($socket, SOL_SOCKET, SO_ERROR);
|
|
|
|
if ($socket_error !== 0) {
|
|
@socket_set_block($socket);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return @socket_set_block($socket);
|
|
}
|
|
|
|
/**
|
|
* Write an entire RRDtool proxy frame.
|
|
*
|
|
* socket_write() may complete only part of a write. This helper retries until
|
|
* all bytes are transferred, and treats zero-byte or failed writes as errors.
|
|
*
|
|
* @param Socket $socket Proxy socket
|
|
* @param string $data Complete encoded frame
|
|
*
|
|
* @return bool Whether every byte was written
|
|
*/
|
|
function rrdtool_proxy_write(Socket $socket, string $data) : bool {
|
|
$length = strlen($data);
|
|
$written = 0;
|
|
|
|
while ($written < $length) {
|
|
set_error_handler(static fn () : bool => true);
|
|
|
|
try {
|
|
$result = socket_write($socket, substr($data, $written));
|
|
} finally {
|
|
restore_error_handler();
|
|
}
|
|
|
|
if ($result === false || $result === 0) {
|
|
return false;
|
|
}
|
|
|
|
$written += $result;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Read one bounded, terminator-delimited RRDtool proxy frame.
|
|
*
|
|
* @param Socket $socket Proxy socket
|
|
* @param string $terminator Protocol frame terminator
|
|
* @param int $max_size Maximum buffered frame size
|
|
*
|
|
* @return string|false Frame contents without the terminator, or false
|
|
*/
|
|
function rrdtool_proxy_read_frame(Socket $socket, string $terminator, int $max_size) : string|false {
|
|
if ($terminator === '' || $max_size < 1) {
|
|
return false;
|
|
}
|
|
|
|
$input = '';
|
|
|
|
while (true) {
|
|
$remaining = $max_size + strlen($terminator) - strlen($input);
|
|
$recv = @socket_read($socket, min(100000, $remaining), PHP_BINARY_READ);
|
|
|
|
if ($recv === false || $recv === '') {
|
|
return false;
|
|
}
|
|
|
|
$input .= $recv;
|
|
$position = strpos($input, $terminator);
|
|
|
|
if ($position !== false) {
|
|
return substr($input, 0, $position);
|
|
}
|
|
|
|
if (strlen($input) > $max_size) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode gzip data without leaking malformed-peer warnings into graph output.
|
|
*
|
|
* @param string $input Compressed packet
|
|
* @param int $max_size Maximum decoded size
|
|
*
|
|
* @return string|false Decoded packet, or false when invalid or oversized
|
|
*/
|
|
function rrdtool_proxy_gzdecode(string $input, int $max_size) : string|false {
|
|
if ($max_size < 1) {
|
|
return false;
|
|
}
|
|
|
|
set_error_handler(static fn () : bool => true);
|
|
|
|
try {
|
|
return gzdecode($input, $max_size);
|
|
} finally {
|
|
restore_error_handler();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode and validate a complete RRDtool proxy response.
|
|
*
|
|
* The terminal status must occur at the end of the final protocol packet.
|
|
* Status-like text in graph or xport payload data is therefore not treated as
|
|
* a protocol boundary.
|
|
*
|
|
* @param string $frame Encoded frame without the sequence terminator
|
|
* @param int $max_size Maximum decoded response size
|
|
*
|
|
* @return array{output:string,status:string,success:bool,raw:string}|false
|
|
*/
|
|
function rrdtool_proxy_decode_response(string $frame, int $max_size = RRD_PROXY_MAX_RESPONSE_SIZE) : array|false {
|
|
if ($max_size < 1 || strlen($frame) > $max_size) {
|
|
return false;
|
|
}
|
|
|
|
$packets = explode(RRD_PROXY_END_OF_PACKET, $frame);
|
|
|
|
if (end($packets) === '') {
|
|
array_pop($packets);
|
|
}
|
|
|
|
if ($packets === []) {
|
|
return false;
|
|
}
|
|
|
|
$decoded_size = 0;
|
|
$decoded = [];
|
|
|
|
foreach ($packets as $packet) {
|
|
$transaction = decrypt($packet);
|
|
|
|
if ($transaction === false) {
|
|
return false;
|
|
}
|
|
|
|
if (str_starts_with($transaction, "\x1f\x8b")) {
|
|
$transaction = rrdtool_proxy_gzdecode($transaction, $max_size - $decoded_size);
|
|
|
|
if ($transaction === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$decoded_size += strlen($transaction);
|
|
$decoded[] = $transaction;
|
|
}
|
|
|
|
$last_packet = array_pop($decoded);
|
|
|
|
if (!preg_match('/(?:^|\r?\n)(OK u(?::[^\r\n]*)?|ERROR:[^\r\n]*)\r?\n?\z/D', $last_packet, $matches, PREG_OFFSET_CAPTURE)) {
|
|
return false;
|
|
}
|
|
|
|
$status_offset = $matches[1][1];
|
|
$status = $matches[1][0];
|
|
$packet_output = substr($last_packet, 0, $status_offset);
|
|
$output = implode('', $decoded) . rtrim($packet_output, "\r\n");
|
|
$raw = $output . ($output === '' ? '' : "\n") . $status;
|
|
|
|
return [
|
|
'output' => $output,
|
|
'status' => $status,
|
|
'success' => str_starts_with($status, 'OK u'),
|
|
'raw' => $raw
|
|
];
|
|
}
|
|
|
|
function __rrd_proxy_init(string $logopt = 'WEBLOG') : mixed {
|
|
global $encryption;
|
|
$encryption = true;
|
|
|
|
$load_balancing = read_config_option('rrdp_load_balancing') == 'on' ? true : false;
|
|
|
|
$portp = intval(read_config_option('rrdp_port'));
|
|
$portb = intval(read_config_option('rrdp_port_backup'));
|
|
$servera = read_config_option('rrdp_server');
|
|
$serverb = read_config_option('rrdp_server_backup');
|
|
|
|
if ($load_balancing) {
|
|
$rrdp_id = random_int(1,2);
|
|
$server = ($rrdp_id == 1) ? $servera : $serverb;
|
|
$port = ($rrdp_id == 1) ? $portp : $portb;
|
|
} else {
|
|
$server = read_config_option('rrdp_server');
|
|
$port = intval(read_config_option('rrdp_port'));
|
|
$rrdp_id = 1;
|
|
}
|
|
|
|
// Detect address family based on server address for IPv6 support
|
|
$socket_family = filter_var(trim($server, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? AF_INET6 : AF_INET;
|
|
|
|
$rrdp_socket = @socket_create($socket_family, SOCK_STREAM, SOL_TCP);
|
|
|
|
if ($rrdp_socket === false) {
|
|
cacti_log('CACTI2RRDP ERROR: Unable to create socket to connect to RRDtool Proxy Server', false, $logopt, POLLER_VERBOSITY_LOW);
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!rrdtool_proxy_set_timeouts($rrdp_socket)) {
|
|
cacti_log('CACTI2RRDP ERROR: Unable to configure socket time-outs.', false, $logopt, POLLER_VERBOSITY_LOW);
|
|
@socket_close($rrdp_socket);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Strip brackets from IPv6 addresses for socket_connect
|
|
$connect_server = trim($server, '[]');
|
|
|
|
$rrdp = rrdtool_proxy_connect($rrdp_socket, $connect_server, $port);
|
|
|
|
if ($rrdp === false) {
|
|
// log entry ...
|
|
cacti_log('CACTI2RRDP ERROR: Unable to connect to RRDtool Proxy Server #' . $rrdp_id, false, $logopt, POLLER_VERBOSITY_LOW);
|
|
|
|
if ($load_balancing) {
|
|
// Close the failed socket before creating a new one for the backup server
|
|
@socket_close($rrdp_socket);
|
|
|
|
$rrdp_id = ($rrdp_id == 1) ? 2 : 1;
|
|
$server = ($rrdp_id == 1) ? $servera : $serverb;
|
|
$port = ($rrdp_id == 1) ? $portp : $portb;
|
|
|
|
// Re-detect address family for backup server (may differ from primary)
|
|
$socket_family = filter_var(trim($server, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? AF_INET6 : AF_INET;
|
|
$connect_server = trim($server, '[]');
|
|
|
|
$rrdp_socket = @socket_create($socket_family, SOCK_STREAM, SOL_TCP);
|
|
|
|
if ($rrdp_socket === false) {
|
|
cacti_log('CACTI2RRDP ERROR: Unable to create socket to connect to RRDtool Proxy Server #' . $rrdp_id, false, $logopt, POLLER_VERBOSITY_LOW);
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!rrdtool_proxy_set_timeouts($rrdp_socket)) {
|
|
cacti_log('CACTI2RRDP ERROR: Unable to configure backup socket time-outs.', false, $logopt, POLLER_VERBOSITY_LOW);
|
|
@socket_close($rrdp_socket);
|
|
|
|
return false;
|
|
}
|
|
|
|
$rrdp = rrdtool_proxy_connect($rrdp_socket, $connect_server, $port);
|
|
|
|
if ($rrdp === false) {
|
|
cacti_log('CACTI2RRDP ERROR: Unable to connect to RRDtool Proxy Server #' . $rrdp_id, false, $logopt, POLLER_VERBOSITY_LOW);
|
|
@socket_close($rrdp_socket);
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
@socket_close($rrdp_socket);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$rrdp_fingerprint = ($rrdp_id == 1) ? read_config_option('rrdp_fingerprint') : read_config_option('rrdp_fingerprint_backup');
|
|
|
|
if (!rrdtool_proxy_write($rrdp_socket, read_config_option('rsa_public_key') . RRD_PROXY_END_OF_SEQUENCE)) {
|
|
cacti_log('CACTI2RRDP ERROR: Public RSA Key Exchange - unable to send local key.', false, $logopt, POLLER_VERBOSITY_LOW);
|
|
@socket_close($rrdp_socket);
|
|
|
|
return false;
|
|
}
|
|
|
|
// read public key being returned by the proxy server
|
|
$rrdp_public_key = rrdtool_proxy_read_frame($rrdp_socket, RRD_PROXY_END_OF_SEQUENCE, RRD_PROXY_MAX_KEY_SIZE);
|
|
|
|
if ($rrdp_public_key === false || trim($rrdp_public_key) === '') {
|
|
cacti_log('CACTI2RRDP ERROR: Public RSA Key Exchange failed - no key received.', false, $logopt, POLLER_VERBOSITY_LOW);
|
|
@socket_close($rrdp_socket);
|
|
|
|
return false;
|
|
}
|
|
|
|
$rrdp_public_key = trim($rrdp_public_key);
|
|
|
|
try {
|
|
$public = RSA::loadPublicKey($rrdp_public_key);
|
|
} catch (Throwable $e) {
|
|
cacti_log('CACTI2RRDP ERROR: Public RSA Key Exchange returned an invalid key.', false, $logopt, POLLER_VERBOSITY_LOW);
|
|
@socket_close($rrdp_socket);
|
|
|
|
return false;
|
|
}
|
|
|
|
$fingerprint = $public->getFingerprint('md5');
|
|
|
|
if (!is_string($rrdp_fingerprint) || !hash_equals($rrdp_fingerprint, $fingerprint)) {
|
|
cacti_log('CACTI2RRDP ERROR: Mismatch RSA Fingerprint.', false, $logopt, POLLER_VERBOSITY_LOW);
|
|
@socket_close($rrdp_socket);
|
|
|
|
return false;
|
|
} else {
|
|
$rrdproxy = [$rrdp_socket, $rrdp_public_key];
|
|
|
|
// set the rrdtool default font
|
|
if (read_config_option('path_rrdtool_default_font')) {
|
|
rrdtool_execute('setenv RRD_DEFAULT_FONT ' . cacti_escapeshellarg((string) read_config_option('path_rrdtool_default_font')), false, RRDTOOL_OUTPUT_NULL, $rrdproxy, $logopt);
|
|
}
|
|
|
|
return $rrdproxy;
|
|
}
|
|
}
|
|
|
|
function rrd_close() : void {
|
|
global $config;
|
|
|
|
$args = func_get_args();
|
|
|
|
$local_storage = (isset($config['local_storage']) && $config['local_storage'] === true) ? true : false;
|
|
|
|
$function = ($local_storage === false && read_config_option('storage_location')) ? '__rrd_proxy_close' : '__rrd_close';
|
|
|
|
call_user_func_array($function, $args);
|
|
}
|
|
|
|
function __rrd_close(mixed $rrdtool_pipe) : void {
|
|
if (rrdtool_is_process($rrdtool_pipe)) {
|
|
if ($rrdtool_pipe->alive && is_resource($rrdtool_pipe->stdin)) {
|
|
rrdtool_write_all($rrdtool_pipe->stdin, "quit\n");
|
|
}
|
|
|
|
foreach (['stdin', 'stdout', 'stderr'] as $stream) {
|
|
if (is_resource($rrdtool_pipe->{$stream})) {
|
|
fclose($rrdtool_pipe->{$stream});
|
|
}
|
|
}
|
|
|
|
if (is_resource($rrdtool_pipe->process)) {
|
|
proc_close($rrdtool_pipe->process);
|
|
}
|
|
|
|
$rrdtool_pipe->alive = false;
|
|
} elseif (is_resource($rrdtool_pipe)) {
|
|
// Compatibility with a pipe opened by an older plugin.
|
|
pclose($rrdtool_pipe);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine whether a value is a local RRDtool process handle.
|
|
*
|
|
* @param mixed $process Candidate process handle
|
|
*
|
|
* @return bool Whether the value contains every required process resource
|
|
*/
|
|
function rrdtool_is_process(mixed $process) : bool {
|
|
return is_object($process)
|
|
&& isset($process->process, $process->stdin, $process->stdout, $process->stderr, $process->alive);
|
|
}
|
|
|
|
/**
|
|
* Write a complete command to RRDtool, including when fwrite() is partial.
|
|
*
|
|
* @param mixed $stream Writable process stream
|
|
* @param string $data Complete bytes to write
|
|
*
|
|
* @return bool Whether every byte was written and flushed
|
|
*/
|
|
function rrdtool_write_all(mixed $stream, string $data) : bool {
|
|
if (!is_resource($stream)) {
|
|
return false;
|
|
}
|
|
|
|
$offset = 0;
|
|
$length = strlen($data);
|
|
|
|
while ($offset < $length) {
|
|
$written = @fwrite($stream, substr($data, $offset));
|
|
|
|
if ($written === false || $written === 0) {
|
|
return false;
|
|
}
|
|
|
|
$offset += $written;
|
|
}
|
|
|
|
return fflush($stream);
|
|
}
|
|
|
|
/**
|
|
* Convert Cacti's shell-quoted arguments to RRDtool stdin quoting.
|
|
*
|
|
* RRDtool's interactive parser accepts double-quoted arguments, but shell
|
|
* escaping for an apostrophe (for example, 'one'\''s') is not valid in that
|
|
* parser. This converter recognizes only the single-quoted form produced by
|
|
* escapeshellarg(); it does not attempt to interpret arbitrary shell syntax.
|
|
*
|
|
* @return string|false The stdin-safe command, or false for an invalid command
|
|
*/
|
|
function rrdtool_prepare_stdin_command(string $command_line) : string|false {
|
|
if (strlen($command_line) > RRDTOOL_MAX_COMMAND_BYTES || strpbrk($command_line, "\0\r\n") !== false) {
|
|
return false;
|
|
}
|
|
|
|
if (!preg_match('/^\s*([A-Za-z][A-Za-z0-9-]*)\b/', $command_line, $matches)) {
|
|
return false;
|
|
}
|
|
|
|
$commands = [
|
|
'create', 'update', 'updatev', 'graph', 'graphv', 'dump', 'restore',
|
|
'last', 'lastupdate', 'first', 'info', 'list', 'fetch', 'tune',
|
|
'resize', 'xport', 'flushcached', 'ls', 'cd', 'mkdir', 'pwd'
|
|
];
|
|
|
|
if (!in_array(strtolower($matches[1]), $commands, true)) {
|
|
return false;
|
|
}
|
|
|
|
$prepared = '';
|
|
$length = strlen($command_line);
|
|
|
|
for ($offset = 0; $offset < $length; $offset++) {
|
|
if ($command_line[$offset] !== "'") {
|
|
$prepared .= $command_line[$offset];
|
|
|
|
continue;
|
|
}
|
|
|
|
$value = '';
|
|
$closed = false;
|
|
$offset++;
|
|
|
|
while ($offset < $length) {
|
|
$quote = strpos($command_line, "'", $offset);
|
|
|
|
if ($quote === false) {
|
|
return false;
|
|
}
|
|
|
|
$value .= substr($command_line, $offset, $quote - $offset);
|
|
$offset = $quote + 1;
|
|
|
|
if (substr($command_line, $offset, 3) === "\\''") {
|
|
$value .= "'";
|
|
$offset += 3;
|
|
|
|
continue;
|
|
}
|
|
|
|
$closed = true;
|
|
$offset--;
|
|
|
|
break;
|
|
}
|
|
|
|
if (!$closed) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* RRDtool control codes such as \: and \n must reach the graph parser
|
|
* verbatim. Escaping the backslash leaves a legend like "IO Wait\:"
|
|
* ending in a dangling control code, which RRDtool refuses, so only the
|
|
* delimiter is escaped here. A value ending in a backslash, or holding
|
|
* one immediately before a double quote, has no unambiguous form inside
|
|
* a quoted token and is rejected instead.
|
|
*/
|
|
if (str_ends_with($value, '\\') || str_contains($value, '\\"')) {
|
|
return false;
|
|
}
|
|
|
|
$prepared .= '"' . str_replace('"', '\\"', $value) . '"';
|
|
}
|
|
|
|
return $prepared;
|
|
}
|
|
|
|
function __rrd_proxy_close(mixed $rrdp) : void {
|
|
if (is_array($rrdp) && isset($rrdp[0], $rrdp[1]) && $rrdp[0] instanceof Socket) {
|
|
try {
|
|
rrdtool_proxy_write($rrdp[0], encrypt('quit', $rrdp[1]) . RRD_PROXY_END_OF_SEQUENCE);
|
|
} catch (Throwable $e) {
|
|
// The connection still has to be closed when request encoding fails.
|
|
} finally {
|
|
@socket_shutdown($rrdp[0], 2);
|
|
@socket_close($rrdp[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function encrypt(string $output, string $rsa_key) : string {
|
|
global $encryption;
|
|
|
|
if ($encryption) {
|
|
/** @var \phpseclib4\Crypt\RSA\PublicKey $public loadPublicKey() returns the RSA public key implementation, which exposes encrypt() */
|
|
$public = RSA::loadPublicKey($rsa_key);
|
|
$aes = new Rijndael('cbc');
|
|
$aes_key = random_bytes(32);
|
|
|
|
$aes->setKey($aes_key);
|
|
$aes->setIV(str_repeat("\0", 16));
|
|
$ciphertext = base64_encode($aes->encrypt($output));
|
|
$aes_key = base64_encode($public->encrypt($aes_key));
|
|
$aes_key_length = str_pad(dechex(strlen($aes_key)), 3, '0', STR_PAD_LEFT);
|
|
|
|
return $aes_key_length . $aes_key . $ciphertext;
|
|
} else {
|
|
return $output;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypt one encrypted RRDtool proxy packet.
|
|
*
|
|
* @param string $input Encoded packet
|
|
* @param string $rsa_private_key Local RSA private key
|
|
*
|
|
* @return string|false Plaintext, or false when the packet is malformed
|
|
*/
|
|
function rrdtool_proxy_decrypt(string $input, string $rsa_private_key) : string|false {
|
|
if (strlen($input) < 4 || !ctype_xdigit(substr($input, 0, 3))) {
|
|
return false;
|
|
}
|
|
|
|
$aes_key_length = hexdec(substr($input, 0, 3));
|
|
|
|
if ($aes_key_length < 1 || strlen($input) <= 3 + $aes_key_length) {
|
|
return false;
|
|
}
|
|
|
|
$aes_key = base64_decode(substr($input, 3, $aes_key_length), true);
|
|
$ciphertext = base64_decode(substr($input, 3 + $aes_key_length), true);
|
|
|
|
if ($aes_key === false || $aes_key === '' || $ciphertext === false) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
/** @var \phpseclib4\Crypt\RSA\PrivateKey $private loadPrivateKey() returns the RSA private key implementation, which exposes decrypt() */
|
|
$private = RSA::loadPrivateKey($rsa_private_key);
|
|
$aes = new Rijndael('cbc');
|
|
$aes_key = $private->decrypt($aes_key);
|
|
|
|
if (!is_string($aes_key) || $aes_key === '') {
|
|
return false;
|
|
}
|
|
|
|
// phpseclib 2 truncated oversized Rijndael keys to 256 bits.
|
|
if (strlen($aes_key) > 32) {
|
|
$aes_key = substr($aes_key, 0, 32);
|
|
}
|
|
|
|
$aes->setKey($aes_key);
|
|
$aes->setIV(str_repeat("\0", 16));
|
|
|
|
return $aes->decrypt($ciphertext);
|
|
} catch (Throwable $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function decrypt(string $input) : string|false {
|
|
global $encryption;
|
|
|
|
if (!$encryption) {
|
|
return $input;
|
|
}
|
|
|
|
return rrdtool_proxy_decrypt($input, (string) read_config_option('rsa_private_key'));
|
|
}
|
|
|
|
function rrdtool_execute() : mixed {
|
|
global $config;
|
|
|
|
$args = func_get_args();
|
|
|
|
$local_storage = (isset($config['local_storage']) && $config['local_storage'] === true) ? true : false;
|
|
|
|
$function = ($local_storage === false && read_config_option('storage_location')) ? '__rrd_proxy_execute' : '__rrd_execute';
|
|
|
|
return call_user_func_array($function, $args);
|
|
}
|
|
|
|
/**
|
|
* Build a non-sensitive identifier for an RRDtool command.
|
|
*
|
|
* RRDtool command lines can contain paths, graph labels, device metadata, and
|
|
* credentials substituted by legacy templates. Logging the complete command
|
|
* therefore exposes considerably more than is needed to correlate failures.
|
|
* The operation, byte count, and digest retain that correlation without
|
|
* copying command arguments into the log.
|
|
*
|
|
* @param string $command_line The complete RRDtool command line
|
|
*
|
|
* @return string A safe operation/length/digest tuple
|
|
*/
|
|
function rrdtool_command_log_context(string $command_line) : string {
|
|
$command = 'unknown';
|
|
$trimmed = ltrim($command_line, " \t");
|
|
|
|
if (preg_match('/^([A-Za-z0-9_-]+)/', $trimmed, $matches)) {
|
|
$command = strtolower($matches[1]);
|
|
}
|
|
|
|
return sprintf(
|
|
'command=%s bytes=%d sha256=%s',
|
|
$command,
|
|
strlen($command_line),
|
|
hash('sha256', $command_line)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Build an RRDtool command from a trusted operation and isolated arguments.
|
|
*
|
|
* @param array $command One operation followed by zero or more arguments
|
|
*
|
|
* @return string|false A command suitable for stdin conversion, or false
|
|
*/
|
|
function rrdtool_build_command(array $command) : string|false {
|
|
$operation = array_shift($command);
|
|
|
|
if (!is_string($operation) || $operation === '') {
|
|
return false;
|
|
}
|
|
|
|
foreach ($command as $argument) {
|
|
if (!is_string($argument)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return $operation . ($command === [] ? '' : ' ' . implode(' ', array_map('cacti_escapeshellarg', $command)));
|
|
}
|
|
|
|
/**
|
|
* Select the locale inherited by a newly launched RRDtool process.
|
|
*
|
|
* RRDtool formats fetch/info/xport numbers for the current locale. Cacti's
|
|
* parsers require a decimal point for those machine-readable responses.
|
|
*
|
|
* @return string|false English for numeric output, otherwise the Cacti locale
|
|
*/
|
|
function rrdtool_command_language(string $command_line) : string|false {
|
|
$operation = strtolower(strtok(ltrim($command_line), " \t") ?: '');
|
|
|
|
return in_array($operation, ['fetch', 'info', 'xport'], true) ? 'en' : false;
|
|
}
|
|
|
|
/**
|
|
* Execute one command over an RRDtool stdin process and wait for its status.
|
|
*
|
|
* @return array{success: bool, output: string, error: string}
|
|
*/
|
|
/**
|
|
* Relaunch a died RRDtool process in place.
|
|
*
|
|
* The poller opens one process per run and reuses it for every update, so a
|
|
* process that dies partway through would otherwise take the rest of the run
|
|
* with it. The replacement is written back into the same object so every holder
|
|
* of the handle recovers.
|
|
*
|
|
* @param object $process RRDtool process handle to revive
|
|
*
|
|
* @return bool Whether a working process is available afterwards
|
|
*/
|
|
function rrdtool_process_restart(object $process) : bool {
|
|
if (!rrdtool_is_process($process)) {
|
|
return false;
|
|
}
|
|
|
|
$attempts = isset($process->restarts) ? (int) $process->restarts : 0;
|
|
|
|
if ($attempts >= RRDTOOL_MAX_RESTARTS) {
|
|
return false;
|
|
}
|
|
|
|
foreach (['stdin', 'stdout', 'stderr'] as $pipe) {
|
|
if (isset($process->$pipe) && is_resource($process->$pipe)) {
|
|
fclose($process->$pipe);
|
|
}
|
|
}
|
|
|
|
if (isset($process->process) && is_resource($process->process)) {
|
|
proc_close($process->process);
|
|
}
|
|
|
|
$replacement = __rrd_init((bool) ($process->output_to_term ?? false));
|
|
|
|
if (!rrdtool_is_process($replacement)) {
|
|
$process->alive = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
$process->process = $replacement->process;
|
|
$process->stdin = $replacement->stdin;
|
|
$process->stdout = $replacement->stdout;
|
|
$process->stderr = $replacement->stderr;
|
|
$process->alive = true;
|
|
$process->restarts = $attempts + 1;
|
|
|
|
cacti_log(sprintf('WARNING: Detected RRDtool Crash, restarted the process (attempt %d of %d).', $attempts + 1, RRDTOOL_MAX_RESTARTS), false, 'RRDTOOL');
|
|
|
|
if (function_exists('debounce_run_notification')) {
|
|
debounce_run_notification('rrdtool_command_crash');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function rrdtool_process_command(object $process, string $command_line, float $timeout = 60.0) : array {
|
|
$result = ['success' => false, 'output' => '', 'error' => 'RRDtool process is unavailable.'];
|
|
|
|
if (!rrdtool_is_process($process)) {
|
|
return $result;
|
|
}
|
|
|
|
// A died process used to be reopened and the command retried; without that
|
|
// every remaining update in the run fails.
|
|
if (!$process->alive && !rrdtool_process_restart($process)) {
|
|
return $result;
|
|
}
|
|
|
|
$prepared_command = rrdtool_prepare_stdin_command($command_line);
|
|
|
|
if ($prepared_command === false) {
|
|
$result['error'] = 'RRDtool command contains an invalid operation, quote, or framing character.';
|
|
|
|
return $result;
|
|
}
|
|
|
|
if (!rrdtool_write_all($process->stdin, $prepared_command . "\n")) {
|
|
$result['error'] = 'Unable to write the complete RRDtool command.';
|
|
$process->alive = false;
|
|
|
|
return $result;
|
|
}
|
|
|
|
$deadline = microtime(true) + max(0.1, $timeout);
|
|
$stdout = '';
|
|
$stderr = '';
|
|
$status = null;
|
|
|
|
while (microtime(true) < $deadline) {
|
|
$read = [];
|
|
|
|
if (is_resource($process->stdout) && !feof($process->stdout)) {
|
|
$read[] = $process->stdout;
|
|
}
|
|
|
|
if (is_resource($process->stderr) && !feof($process->stderr)) {
|
|
$read[] = $process->stderr;
|
|
}
|
|
|
|
if ($read === []) {
|
|
break;
|
|
}
|
|
|
|
$write = null;
|
|
$except = null;
|
|
$ready = @stream_select($read, $write, $except, 0, 200000);
|
|
|
|
if ($ready === false) {
|
|
$result['error'] = 'Unable to read the RRDtool response.';
|
|
|
|
break;
|
|
}
|
|
|
|
foreach ($read as $stream) {
|
|
$chunk = fread($stream, 1048576);
|
|
|
|
if ($chunk === false) {
|
|
continue;
|
|
}
|
|
|
|
if ($stream === $process->stdout) {
|
|
$stdout .= $chunk;
|
|
} else {
|
|
$stderr .= $chunk;
|
|
}
|
|
}
|
|
|
|
if (strlen($stdout) + strlen($stderr) > RRDTOOL_MAX_RESPONSE_BYTES) {
|
|
$result['error'] = 'RRDtool response exceeded the configured safety limit.';
|
|
$process->alive = false;
|
|
|
|
if (is_resource($process->process)) {
|
|
proc_terminate($process->process);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
if (preg_match('/(OK(?: u:[^\r\n]*)?|ERROR:[^\r\n]*)\r?\n?$/', $stdout, $matches, PREG_OFFSET_CAPTURE)) {
|
|
$status = $matches[1][0];
|
|
$stdout = substr($stdout, 0, $matches[0][1]);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
$result['output'] = $stdout;
|
|
$result['error'] = trim($stderr);
|
|
|
|
if ($status === null) {
|
|
if ($result['error'] === '') {
|
|
$result['error'] = 'RRDtool did not return a terminal status before the timeout.';
|
|
}
|
|
|
|
$process->alive = false;
|
|
|
|
if (is_resource($process->process)) {
|
|
proc_terminate($process->process);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
if (str_starts_with($status, 'ERROR:')) {
|
|
$result['error'] = trim($status . ($result['error'] !== '' ? "\n" . $result['error'] : ''));
|
|
|
|
return $result;
|
|
}
|
|
|
|
$result['success'] = true;
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Convert a process result to the long-standing RRDTOOL_OUTPUT_* contract.
|
|
*/
|
|
function rrdtool_format_result(array $result, int $output_flag) : mixed {
|
|
if (!$result['success']) {
|
|
if ($output_flag === RRDTOOL_OUTPUT_RETURN_STDERR) {
|
|
return $result['error'];
|
|
}
|
|
|
|
if ($output_flag === RRDTOOL_OUTPUT_STDERR && $result['error'] !== '') {
|
|
print $result['error'];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
switch ($output_flag) {
|
|
case RRDTOOL_OUTPUT_NULL:
|
|
case RRDTOOL_OUTPUT_BOOLEAN:
|
|
return true;
|
|
case RRDTOOL_OUTPUT_STDOUT:
|
|
case RRDTOOL_OUTPUT_GRAPH_DATA:
|
|
return $result['output'];
|
|
case RRDTOOL_OUTPUT_RETURN_STDERR:
|
|
return $result['error'] !== '' ? $result['error'] : $result['output'];
|
|
case RRDTOOL_OUTPUT_STDERR:
|
|
if ($result['error'] !== '') {
|
|
print $result['error'];
|
|
}
|
|
|
|
if (substr($result['output'], 1, 3) === 'PNG') {
|
|
return 'OK';
|
|
}
|
|
|
|
if (str_starts_with($result['output'], '<?xml')) {
|
|
return 'SVG/XML Output OK';
|
|
}
|
|
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute an RRDtool command and return the output.
|
|
*
|
|
* @param string|array $command_line The RRDtool command to execute. An array is quoted here,
|
|
* one element per argument. A caller passing a string quotes
|
|
* each variable argument with cacti_escapeshellarg() as it
|
|
* builds the line; the assembled line is never quoted
|
|
* @param bool $log_to_stdout Whether to echo output to stdout
|
|
* @param int $output_flag Output format constant (RRDTOOL_OUTPUT_*)
|
|
* @param mixed $rrdtool_pipe An open RRDtool pipe resource, or null
|
|
* @param string $logopt Logging context identifier
|
|
*
|
|
* @return mixed The command output in the requested format
|
|
*/
|
|
function __rrd_execute(string|array $command_line, bool $log_to_stdout, int $output_flag = RRDTOOL_OUTPUT_STDOUT, mixed $rrdtool_pipe = null, string $logopt = 'WEBLOG') : mixed {
|
|
if (is_array($command_line)) {
|
|
$command_line = rrdtool_build_command($command_line);
|
|
|
|
if ($command_line === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$command_line = str_replace("\\\n", ' ', $command_line);
|
|
|
|
/* Defense in depth: after all placeholder and title substitution, strip any
|
|
* control characters from the assembled command before it is written to the
|
|
* rrdtool pipe or executed. A CR/LF in a field value would otherwise inject
|
|
* an independent rrdtool command over the line based pipe protocol. */
|
|
$command_line = rrd_strip_control_chars($command_line);
|
|
$context = rrdtool_command_log_context($command_line);
|
|
|
|
cacti_log('CACTI2RRD: ' . $context, $log_to_stdout, $logopt, POLLER_VERBOSITY_DEBUG);
|
|
|
|
$language = rrdtool_command_language($command_line);
|
|
|
|
cacti_session_close();
|
|
|
|
$auto_close = !rrdtool_is_process($rrdtool_pipe);
|
|
$process = $auto_close ? __rrd_init(true, $language) : $rrdtool_pipe;
|
|
|
|
if (!rrdtool_is_process($process)) {
|
|
cacti_log('ERROR: RRDtool process unavailable for ' . $context . '.', false, $logopt);
|
|
|
|
return false;
|
|
}
|
|
|
|
$result = rrdtool_process_command($process, $command_line);
|
|
|
|
if ($auto_close) {
|
|
__rrd_close($process);
|
|
}
|
|
|
|
if (!$result['success']) {
|
|
cacti_log('ERROR: RRDtool rejected ' . $context . ': ' . $result['error'], false, $logopt);
|
|
}
|
|
|
|
return rrdtool_format_result($result, $output_flag);
|
|
}
|
|
function rrdtool_trim_output(string &$output) : void {
|
|
global $user_time, $system_time, $real_time;
|
|
|
|
/**
|
|
* When using RRDtool with proc_open for long strings
|
|
* and using the '-' to handle standard in from inside
|
|
* the process, RRDtool automatically appends stderr
|
|
* to stdout for batch programs to parse the output
|
|
* string. So, therefore, we have to prune that
|
|
* output.
|
|
*/
|
|
if (CACTI_SERVER_OS == 'win32') {
|
|
$output = rtrim($output, "OK \n\r");
|
|
} else {
|
|
$okpos = strrpos($output, 'OK u:');
|
|
|
|
if ($okpos !== false) {
|
|
$stats = substr($output, $okpos);
|
|
$line = trim($stats, ' OK');
|
|
$parts = explode(' ', $line);
|
|
|
|
foreach ($parts as $line) {
|
|
$sparts = explode(':', $line);
|
|
|
|
switch($sparts[0]) {
|
|
case 'u':
|
|
$user_time = $sparts[1];
|
|
|
|
break;
|
|
case 's':
|
|
$system_time = $sparts[1];
|
|
|
|
break;
|
|
case 'r':
|
|
$real_time = $sparts[1];
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
$output = substr($output, 0, $okpos);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute an RRDtool command through the remote proxy.
|
|
*
|
|
* @param string|array $command_line RRDtool command string, or one string argument per array element
|
|
* @param bool $log_to_stdout Whether to echo log output
|
|
* @param int $output_flag Requested RRDTOOL_OUTPUT_* result mode
|
|
* @param mixed $rrdp Existing proxy connection tuple, or an empty value
|
|
* @param string $logopt Logging context identifier
|
|
*
|
|
* @return mixed Output in the requested mode, or false when transport/protocol validation fails
|
|
*/
|
|
function __rrd_proxy_execute(string|array $command_line, bool $log_to_stdout, int $output_flag = RRDTOOL_OUTPUT_STDOUT, mixed $rrdp = '', string $logopt = 'WEBLOG') : mixed {
|
|
if (is_array($command_line)) {
|
|
$command_line = rrdtool_build_command($command_line);
|
|
|
|
if ($command_line === false) {
|
|
cacti_log('CACTI2RRDP ERROR: Invalid proxy command argument list.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* WIN32: before sending this command off to rrdtool, get rid
|
|
* of all of the '\' characters. Unix does not care; win32 does.
|
|
* Also make sure to replace all of the fancy "\"s at the end of the line,
|
|
* but make sure not to get rid of the "\n"s that are supposed to be
|
|
* in there (text format)
|
|
*/
|
|
$command_line = str_replace([CACTI_PATH_RRA, "\\\n"], ['.', ' '], $command_line);
|
|
|
|
/* Strip control characters before the command is sent to the rrdtool proxy,
|
|
* so a CR/LF in a field value cannot inject a second command at the proxy
|
|
* pipe. Mirrors the guard in __rrd_execute() for the local path. */
|
|
$command_line = rrd_strip_control_chars($command_line);
|
|
|
|
// output information to the log file if appropriate
|
|
cacti_log('CACTI2RRDP: ' . rrdtool_command_log_context($command_line), $log_to_stdout, $logopt, POLLER_VERBOSITY_DEBUG);
|
|
|
|
// store the last command to provide rrdtool segfault diagnostics
|
|
$last_command = $command_line;
|
|
$rrdp_auto_close = false;
|
|
|
|
if (!$rrdp) {
|
|
$rrdp = __rrd_proxy_init($logopt);
|
|
$rrdp_auto_close = true;
|
|
}
|
|
|
|
if (!$rrdp) {
|
|
cacti_log('CACTI2RRDP ERROR: Unable to connect to RRDtool proxy.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW);
|
|
|
|
return false;
|
|
} else {
|
|
cacti_log('CACTI2RRDP NOTE: Connection to RRDtool proxy has already been established.', $log_to_stdout, $logopt, POLLER_VERBOSITY_DEBUG);
|
|
}
|
|
|
|
if (!is_array($rrdp) || !isset($rrdp[0], $rrdp[1]) || !($rrdp[0] instanceof Socket) || !is_string($rrdp[1])) {
|
|
cacti_log('CACTI2RRDP ERROR: Invalid proxy connection state.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW);
|
|
|
|
return false;
|
|
}
|
|
|
|
$rrdp_socket = $rrdp[0];
|
|
$rrdp_public_key = $rrdp[1];
|
|
|
|
if (strlen($command_line) >= 8192) {
|
|
$compressed = gzencode($command_line, 1);
|
|
|
|
if ($compressed !== false) {
|
|
$command_line = $compressed;
|
|
}
|
|
}
|
|
|
|
try {
|
|
$frame = encrypt($command_line, $rrdp_public_key) . RRD_PROXY_END_OF_SEQUENCE;
|
|
} catch (Throwable $e) {
|
|
cacti_log('CACTI2RRDP ERROR: Unable to encode the proxy request.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW);
|
|
|
|
if ($rrdp_auto_close) {
|
|
__rrd_proxy_close($rrdp);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!rrdtool_proxy_write($rrdp_socket, $frame)) {
|
|
cacti_log('CACTI2RRDP ERROR: Data Transfer - unable to write complete request.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW);
|
|
|
|
if ($rrdp_auto_close) {
|
|
__rrd_proxy_close($rrdp);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
$frame = rrdtool_proxy_read_frame($rrdp_socket, RRD_PROXY_END_OF_SEQUENCE, RRD_PROXY_MAX_RESPONSE_SIZE);
|
|
|
|
if ($frame === false) {
|
|
cacti_log('CACTI2RRDP ERROR: Data Transfer - timed out, closed, or exceeded the response limit.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW);
|
|
|
|
if ($rrdp_auto_close) {
|
|
__rrd_proxy_close($rrdp);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
$response = rrdtool_proxy_decode_response($frame);
|
|
|
|
if ($response === false) {
|
|
cacti_log('CACTI2RRDP ERROR: Malformed, undecodable, or unterminated proxy response.', $log_to_stdout, $logopt, POLLER_VERBOSITY_LOW);
|
|
|
|
if ($rrdp_auto_close) {
|
|
__rrd_proxy_close($rrdp);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
cacti_log('RRDP: ' . $response['raw'], $log_to_stdout, $logopt, POLLER_VERBOSITY_DEBUG);
|
|
|
|
if ($rrdp_auto_close) {
|
|
__rrd_proxy_close($rrdp);
|
|
}
|
|
|
|
switch ($output_flag) {
|
|
case RRDTOOL_OUTPUT_NULL:
|
|
return $response['success'] ? 'OK' : false;
|
|
case RRDTOOL_OUTPUT_STDOUT:
|
|
return $response['success'] ? rtrim($response['output']) : false;
|
|
case RRDTOOL_OUTPUT_GRAPH_DATA:
|
|
// Graph output is binary. The proxy decoder has already removed the
|
|
// protocol terminator, so trimming here could corrupt valid image bytes.
|
|
return $response['success'] ? $response['output'] : false;
|
|
case RRDTOOL_OUTPUT_STDERR:
|
|
if (!$response['success']) {
|
|
print $response['raw'];
|
|
|
|
return false;
|
|
}
|
|
|
|
if (substr($response['output'], 1, 3) == 'PNG') {
|
|
return 'OK';
|
|
}
|
|
|
|
if (str_starts_with($response['output'], 'GIF87')) {
|
|
return 'OK';
|
|
}
|
|
|
|
if (str_starts_with($response['output'], '<?xml')) {
|
|
return 'SVG/XML Output OK';
|
|
}
|
|
|
|
print $response['output'];
|
|
|
|
return true;
|
|
case RRDTOOL_OUTPUT_RETURN_STDERR:
|
|
return $response['raw'];
|
|
case RRDTOOL_OUTPUT_BOOLEAN:
|
|
return $response['success'];
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function rrdtool_function_interface_speed(array $data_local) : string {
|
|
static $speed_cache = [];
|
|
|
|
$cache_key = implode(':', [$data_local['host_id'], $data_local['snmp_query_id'], $data_local['snmp_index']]);
|
|
|
|
if (isset($speed_cache[$cache_key])) {
|
|
return $speed_cache[$cache_key];
|
|
}
|
|
|
|
$fields = db_fetch_assoc_prepared('SELECT field_name, field_value
|
|
FROM host_snmp_cache
|
|
WHERE host_id = ?
|
|
AND snmp_query_id = ?
|
|
AND snmp_index = ?
|
|
AND field_name IN (?, ?)',
|
|
[$data_local['host_id'], $data_local['snmp_query_id'], $data_local['snmp_index'], 'ifHighSpeed', 'ifSpeed']
|
|
);
|
|
$speeds = array_rekey($fields, 'field_name', 'field_value');
|
|
|
|
$ifHighSpeed = $speeds['ifHighSpeed'] ?? false;
|
|
$ifSpeed = $speeds['ifSpeed'] ?? false;
|
|
|
|
if (!empty($ifHighSpeed)) {
|
|
$speed = (int) $ifHighSpeed * 1000000;
|
|
} elseif (!empty($ifSpeed)) {
|
|
$speed = (int) $ifSpeed;
|
|
} else {
|
|
$speed = intval(read_config_option('default_interface_speed'));
|
|
|
|
if (empty($speed)) {
|
|
$speed = '10000000000000';
|
|
} else {
|
|
$speed *= 1000000;
|
|
}
|
|
}
|
|
|
|
$speed_cache[$cache_key] = (string) $speed;
|
|
|
|
return $speed_cache[$cache_key];
|
|
}
|
|
|
|
/**
|
|
* Reject an RRD file path that shows directory traversal or a NUL byte, and
|
|
* confine it to the RRA directory when that base can be resolved.
|
|
*
|
|
* data_source_path is stored in the database and flows into every rrdtool
|
|
* create/fetch/update/graph command. A '..' segment or a NUL byte would let a
|
|
* crafted path escape the RRA directory. The traversal/NUL check is
|
|
* unconditional so a legitimate path is never refused. Containment under the
|
|
* RRA base is enforced only when both the base and the target (or its parent
|
|
* directory, for a not-yet-created file) resolve with realpath(); remote
|
|
* storage, where the path is proxy relative, leaves them unresolved and is not
|
|
* second-guessed.
|
|
*
|
|
* @param string $path The candidate RRD file path.
|
|
* @param string|null $rra_base RRA base directory; defaults to CACTI_PATH_RRA.
|
|
*
|
|
* @return bool True when the path is safe to use.
|
|
*/
|
|
function rrd_check_path(string $path, ?string $rra_base = null) : bool {
|
|
if ($path === '' || strpos($path, "\0") !== false) {
|
|
return false;
|
|
}
|
|
|
|
// '..' between path boundaries (or at either edge) denotes traversal.
|
|
if (preg_match('/(^|[\/\\\\:])\.\.([\/\\\\:]|$)/', $path)) {
|
|
return false;
|
|
}
|
|
|
|
if ($rra_base === null) {
|
|
$rra_base = defined('CACTI_PATH_RRA') ? (string) CACTI_PATH_RRA : '';
|
|
}
|
|
|
|
// When the RRA base cannot be resolved (e.g. remote/proxy storage), the
|
|
// syntactic traversal/NUL checks above are all that apply.
|
|
$base = ($rra_base !== '') ? realpath($rra_base) : false;
|
|
|
|
if ($base === false) {
|
|
return true;
|
|
}
|
|
|
|
/* Resolve the target as strictly as the filesystem allows: realpath() when
|
|
* the file exists (also collapses symlinks), else realpath() of the nearest
|
|
* existing ancestor with the not-yet-created tail appended. A path whose
|
|
* whole tail is missing cannot be resolved that way, so fall back to a
|
|
* lexical absolute form. A relative path we cannot resolve is refused
|
|
* rather than waved through, which was the hole that let a target with a
|
|
* non-existent parent skip confinement (the create flow mkdir()s parents). */
|
|
$real = realpath($path);
|
|
|
|
if ($real === false) {
|
|
$dir = $path;
|
|
$tail = '';
|
|
|
|
while (true) {
|
|
$parent = dirname($dir);
|
|
$tail = ($tail === '') ? basename($dir) : basename($dir) . DIRECTORY_SEPARATOR . $tail;
|
|
|
|
$resolved = realpath($parent);
|
|
|
|
if ($resolved !== false) {
|
|
$real = $resolved . DIRECTORY_SEPARATOR . $tail;
|
|
|
|
break;
|
|
}
|
|
|
|
if ($parent === $dir) {
|
|
// reached the root without an existing ancestor
|
|
$real = (strpos($path, DIRECTORY_SEPARATOR) === 0) ? $path : false;
|
|
|
|
break;
|
|
}
|
|
|
|
$dir = $parent;
|
|
}
|
|
}
|
|
|
|
if ($real === false) {
|
|
return false;
|
|
}
|
|
|
|
$base = rtrim($base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
|
|
|
return strncmp($real . DIRECTORY_SEPARATOR, $base, strlen($base)) === 0;
|
|
}
|
|
|
|
function rrdtool_function_create(int $local_data_id, bool $show_source, mixed $rrdtool_pipe = null) : mixed {
|
|
global $data_source_types, $consolidation_functions, $encryption;
|
|
|
|
include(CACTI_PATH_INCLUDE . '/global_arrays.php');
|
|
|
|
$data_source_path = get_data_source_path($local_data_id, true);
|
|
|
|
if (!rrd_check_path($data_source_path)) {
|
|
cacti_log("ERROR: Refusing unsafe data source path '$data_source_path' for local_data_id $local_data_id", false, 'RRDTOOL');
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* ok, if that passes lets check to make sure an rra does not already
|
|
* exist, the last thing we want to do is overright data!
|
|
*/
|
|
if ($show_source != true) {
|
|
if (rrdtool_file_exists($data_source_path, $rrdtool_pipe)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* the first thing we must do is make sure there is at least one
|
|
* rra associated with this data source... *
|
|
* UPDATE: As of version 0.6.6, we are splitting this up into two
|
|
* SQL strings because of the multiple DS per RRD support. This is
|
|
* not a big deal however since this function gets called once per
|
|
* data source
|
|
*/
|
|
$rras = db_fetch_assoc_prepared('SELECT dtd.rrd_step, dsp.x_files_factor,
|
|
dspr.steps, dspr.rows, dspc.consolidation_function_id,
|
|
(dspr.rows*dspr.steps) AS rra_order
|
|
FROM data_template_data AS dtd
|
|
LEFT JOIN data_source_profiles AS dsp
|
|
ON dtd.data_source_profile_id=dsp.id
|
|
LEFT JOIN data_source_profiles_rra AS dspr
|
|
ON dsp.id=dspr.data_source_profile_id
|
|
LEFT JOIN data_source_profiles_cf AS dspc
|
|
ON dsp.id=dspc.data_source_profile_id
|
|
WHERE dtd.local_data_id = ?
|
|
AND (dspr.steps IS NOT NULL OR dspr.rows IS NOT NULL)
|
|
ORDER BY dspc.consolidation_function_id, rra_order, dspr.steps',
|
|
[$local_data_id]
|
|
);
|
|
|
|
// if we find that this DS has no RRA associated; get out
|
|
if (cacti_sizeof($rras) <= 0) {
|
|
cacti_log("ERROR: There are no RRA's assigned to local_data_id: $local_data_id.");
|
|
|
|
return false;
|
|
}
|
|
|
|
// create the "--step" line
|
|
$create_ds = RRD_NL . '--start 0 --step ' . $rras[0]['rrd_step'] . ' ' . RRD_NL;
|
|
|
|
/**
|
|
* We have to check for Non-Templated Data Source first as they may not include
|
|
* a graph. So, for that case, we need the RRDfile to include all data sources
|
|
*/
|
|
$data_template_id = db_fetch_cell_prepared('SELECT data_template_id
|
|
FROM data_local
|
|
WHERE id = ?',
|
|
[$local_data_id]);
|
|
|
|
if ($data_template_id > 0) {
|
|
$data_sources = db_fetch_assoc_prepared('SELECT DISTINCT dtr.id, dtr.data_source_name, dtr.rrd_heartbeat,
|
|
dtr.rrd_minimum, dtr.rrd_maximum, dtr.data_source_type_id
|
|
FROM data_template_rrd AS dtr
|
|
INNER JOIN graph_templates_item AS gti
|
|
ON dtr.id = gti.task_item_id
|
|
WHERE local_data_id = ?
|
|
ORDER BY local_data_template_rrd_id',
|
|
[$local_data_id]
|
|
);
|
|
} else {
|
|
$data_sources = db_fetch_assoc_prepared('SELECT DISTINCT dtr.id, dtr.data_source_name, dtr.rrd_heartbeat,
|
|
dtr.rrd_minimum, dtr.rrd_maximum, dtr.data_source_type_id
|
|
FROM data_template_rrd AS dtr
|
|
WHERE local_data_id = ?
|
|
ORDER BY local_data_template_rrd_id',
|
|
[$local_data_id]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* ONLY make a new DS entry if:
|
|
*
|
|
* - There are multiple data sources and this item is not the main one.
|
|
* - There are only one data source (then use it)
|
|
*/
|
|
if (cacti_sizeof($data_sources)) {
|
|
$data_local = db_fetch_row_prepared('SELECT host_id,
|
|
snmp_query_id, snmp_index
|
|
FROM data_local
|
|
WHERE id = ?',
|
|
[$local_data_id]
|
|
);
|
|
|
|
$speed = is_array($data_local) ? rrdtool_function_interface_speed($data_local) : (string) intval(read_config_option('default_interface_speed')) * 1000000;
|
|
|
|
foreach ($data_sources as $data_source) {
|
|
// use the cacti ds name by default or the user defined one, if entered
|
|
$data_source_name = get_data_source_item_name($data_source['id']);
|
|
|
|
// Trim the data source maximum
|
|
$data_source['rrd_maximum'] = trim($data_source['rrd_maximum']);
|
|
|
|
if ($data_source['rrd_maximum'] == 'U') {
|
|
// in case no maximum is given, use "Undef" value
|
|
$data_source['rrd_maximum'] = 'U';
|
|
} elseif (str_contains($data_source['rrd_maximum'], '|query_')) {
|
|
// in case a query variable is given, evaluate it
|
|
if ($data_source['rrd_maximum'] == '|query_ifSpeed|' || $data_source['rrd_maximum'] == '|query_ifHighSpeed|') {
|
|
$data_source['rrd_maximum'] = $speed;
|
|
} elseif (is_array($data_local)) {
|
|
$data_source['rrd_maximum'] = substitute_snmp_query_data($data_source['rrd_maximum'], $data_local['host_id'], $data_local['snmp_query_id'], $data_local['snmp_index']);
|
|
}
|
|
} elseif ($data_source['rrd_maximum'] != 'U' && (int)$data_source['rrd_maximum'] <= (int)$data_source['rrd_minimum']) {
|
|
// max > min required, but take care of an "Undef" value
|
|
if ($data_source['data_source_type_id'] == 1 || $data_source['data_source_type_id'] == 4) {
|
|
$data_source['rrd_maximum'] = 'U';
|
|
} else {
|
|
$data_source['rrd_maximum'] = (int)$data_source['rrd_minimum'] + 1;
|
|
}
|
|
}
|
|
|
|
// min==max==0 won't work with rrdtool
|
|
if ($data_source['rrd_minimum'] == 0 && $data_source['rrd_maximum'] == 0) {
|
|
$data_source['rrd_maximum'] = 'U';
|
|
}
|
|
|
|
// the bounds are about to be concatenated onto an rrdtool command line, so
|
|
// never let anything but a number or the literal 'U' reach that sink
|
|
foreach (['rrd_minimum', 'rrd_maximum'] as $bound) {
|
|
if ($data_source[$bound] !== 'U' && !is_numeric($data_source[$bound])) {
|
|
cacti_log("ERROR: Non-numeric $bound '" . $data_source[$bound] . "' for data source '$data_source_name' local_data_id " . $local_data_id, false, 'RRDTOOL');
|
|
|
|
$data_source[$bound] = 'U';
|
|
}
|
|
}
|
|
|
|
$create_ds .= "DS:$data_source_name:" . $data_source_types[$data_source['data_source_type_id']] . ':' . $data_source['rrd_heartbeat'] . ':' . $data_source['rrd_minimum'] . ':' . $data_source['rrd_maximum'] . RRD_NL;
|
|
}
|
|
}
|
|
|
|
$create_rra = '';
|
|
|
|
// loop through each available RRA for this DS
|
|
foreach ($rras as $rra) {
|
|
$create_rra .= 'RRA:' . $consolidation_functions[$rra['consolidation_function_id']] . ':' . $rra['x_files_factor'] . ':' . $rra['steps'] . ':' . $rra['rows'] . RRD_NL;
|
|
}
|
|
|
|
$owner_id = 0;
|
|
$group_id = 0;
|
|
|
|
if (CACTI_SERVER_OS != 'win32') {
|
|
$owner_id = fileowner(CACTI_PATH_RRA);
|
|
$group_id = filegroup(CACTI_PATH_RRA);
|
|
}
|
|
|
|
/**
|
|
* check for structured path configuration, if in place verify directory
|
|
* exists and if not create it.
|
|
*/
|
|
if (read_config_option('extended_paths') == 'on') {
|
|
if (read_config_option('storage_location')) {
|
|
if (rrdtool_execute('is_dir ' . cacti_escapeshellarg(dirname($data_source_path)), true, RRDTOOL_OUTPUT_BOOLEAN, $rrdtool_pipe, 'POLLER') === false) {
|
|
if (rrdtool_execute('mkdir ' . cacti_escapeshellarg(dirname($data_source_path)), true, RRDTOOL_OUTPUT_BOOLEAN, $rrdtool_pipe, 'POLLER') === false) {
|
|
cacti_log("ERROR: Unable to create directory '" . dirname($data_source_path) . "'", false);
|
|
}
|
|
}
|
|
} elseif (!is_dir(dirname($data_source_path))) {
|
|
if (CACTI_WEB == false || is_writable(CACTI_PATH_RRA)) {
|
|
if (mkdir(dirname($data_source_path), 0775, true)) {
|
|
if (CACTI_SERVER_OS != 'win32' && posix_getuid() == 0) {
|
|
$success = true;
|
|
$paths = explode('/', str_replace(CACTI_PATH_RRA, '/', dirname($data_source_path)));
|
|
$spath = '';
|
|
|
|
foreach ($paths as $path) {
|
|
if ($path == '') {
|
|
continue;
|
|
}
|
|
|
|
$spath .= '/' . $path;
|
|
|
|
$powner_id = fileowner(CACTI_PATH_RRA . $spath);
|
|
$pgroup_id = filegroup(CACTI_PATH_RRA . $spath);
|
|
|
|
if ($powner_id !== false && $owner_id !== false && $powner_id != $owner_id) {
|
|
$success = chown(CACTI_PATH_RRA . $spath, $owner_id);
|
|
}
|
|
|
|
if ($pgroup_id !== false && $group_id !== false && $pgroup_id != $group_id && $success) {
|
|
$success = chgrp(CACTI_PATH_RRA . $spath, $group_id);
|
|
}
|
|
|
|
if (!$success) {
|
|
cacti_log("ERROR: Unable to set directory permissions for '" . CACTI_PATH_RRA . $spath . "'", false);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
cacti_log("ERROR: Unable to create directory '" . dirname($data_source_path) . "'", false);
|
|
}
|
|
} else {
|
|
cacti_log("WARNING: Poller has not created structured path '" . dirname($data_source_path) . "' yet.", false);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($show_source == true) {
|
|
return read_config_option('path_rrdtool') . ' create' . RRD_NL . cacti_escapeshellarg($data_source_path) . $create_ds . $create_rra;
|
|
} else {
|
|
$success = rrdtool_execute('create ' . cacti_escapeshellarg($data_source_path) . " $create_ds$create_rra", true, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe, 'POLLER');
|
|
|
|
if (CACTI_SERVER_OS != 'win32' && posix_getuid() == 0) {
|
|
if (file_exists($data_source_path)) {
|
|
chown($data_source_path, (int) $owner_id);
|
|
chgrp($data_source_path, (int) $group_id);
|
|
} else {
|
|
cacti_log('WARNING: RRDCreate failed for ' . rrdtool_command_log_context('create ' . $data_source_path), false, 'POLLER');
|
|
}
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
}
|
|
|
|
function rrdtool_function_update(array $update_cache_array, mixed $rrdtool_pipe = null) : int {
|
|
// lets count the number of rrd files processed
|
|
$rrds_processed = 0;
|
|
|
|
foreach ($update_cache_array as $rrd_path => $rrd_fields) {
|
|
if (cacti_sizeof($rrd_fields['times'])) {
|
|
$file_exists = rrdtool_file_exists($rrd_path, $rrdtool_pipe);
|
|
|
|
ksort($rrd_fields['times']);
|
|
|
|
if ($file_exists === false) {
|
|
rrdtool_function_create($rrd_fields['local_data_id'], false, $rrdtool_pipe);
|
|
}
|
|
|
|
foreach ($rrd_fields['times'] as $update_time => $field_array) {
|
|
if (empty($update_time)) {
|
|
// default the rrdupdate time to now
|
|
$rrd_update_values = 'N:';
|
|
} else {
|
|
$rrd_update_values = $update_time . ':';
|
|
}
|
|
|
|
$rrd_update_template = '';
|
|
|
|
foreach ($field_array as $field_name => $value) {
|
|
if ($rrd_update_template != '') {
|
|
$rrd_update_template .= ':';
|
|
$rrd_update_values .= ':';
|
|
}
|
|
|
|
$rrd_update_template .= $field_name;
|
|
|
|
// if we have "invalid data", give rrdtool an Unknown (U)
|
|
if (!isset($value) || !is_numeric($value)) {
|
|
$value = 'U';
|
|
}
|
|
|
|
$rrd_update_values .= $value;
|
|
}
|
|
|
|
if (cacti_version_compare(get_rrdtool_version(), '1.5', '>=')) {
|
|
$update_options = '--skip-past-updates';
|
|
} else {
|
|
$update_options = '';
|
|
}
|
|
|
|
$update_result = rrdtool_execute('update ' . cacti_escapeshellarg($rrd_path) . " $update_options --template $rrd_update_template $rrd_update_values", true, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe, 'POLLER');
|
|
|
|
if ($update_result !== false) {
|
|
$rrds_processed++;
|
|
} else {
|
|
cacti_log('ERROR: RRDtool update failed for ' . rrdtool_command_log_context('update ' . $rrd_path), false, 'POLLER');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $rrds_processed;
|
|
}
|
|
|
|
function rrdtool_function_tune(array $rrd_tune_array) : void {
|
|
global $data_source_types;
|
|
|
|
include(CACTI_PATH_INCLUDE . '/global_arrays.php');
|
|
|
|
$data_source_name = get_data_source_item_name($rrd_tune_array['data_source_id']);
|
|
$data_source_type = $data_source_types[$rrd_tune_array['data-source-type']];
|
|
$data_source_path = get_data_source_path($rrd_tune_array['data_source_id'], true);
|
|
|
|
$rrd_tune = '';
|
|
|
|
if ($rrd_tune_array['heartbeat'] != '') {
|
|
$rrd_tune .= ' --heartbeat ' . cacti_escapeshellarg($data_source_name . ':' . $rrd_tune_array['heartbeat']);
|
|
}
|
|
|
|
if ($rrd_tune_array['minimum'] != '') {
|
|
$rrd_tune .= ' --minimum ' . cacti_escapeshellarg($data_source_name . ':' . $rrd_tune_array['minimum']);
|
|
}
|
|
|
|
if ($rrd_tune_array['maximum'] != '') {
|
|
$rrd_tune .= ' --maximum ' . cacti_escapeshellarg($data_source_name . ':' . $rrd_tune_array['maximum']);
|
|
}
|
|
|
|
if ($rrd_tune_array['data-source-type'] != '') {
|
|
$rrd_tune .= ' --data-source-type ' . cacti_escapeshellarg($data_source_name . ':' . $data_source_type);
|
|
}
|
|
|
|
if ($rrd_tune_array['data-source-rename'] != '') {
|
|
$rrd_tune .= ' --data-source-rename ' . cacti_escapeshellarg($data_source_name . ':' . $rrd_tune_array['data-source-rename']);
|
|
}
|
|
|
|
if ($rrd_tune != '') {
|
|
if (file_exists($data_source_path) == true) {
|
|
$result = rrdtool_execute(
|
|
'tune ' . cacti_escapeshellarg($data_source_path) . $rrd_tune,
|
|
false,
|
|
RRDTOOL_OUTPUT_BOOLEAN,
|
|
null,
|
|
'WEBLOG'
|
|
);
|
|
|
|
if ($result === false) {
|
|
cacti_log('ERROR: Unable to tune ' . rrdtool_command_log_context('tune ' . $data_source_path) . '.', false, 'WEBLOG');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert RRDtool fetch output into Cacti's timestamp-indexed representation.
|
|
*
|
|
* RRDtool emits one bucket after the requested end. The graph window is
|
|
* end-inclusive, so that bucket must not reach percentile and summation users.
|
|
*
|
|
* @param string $output Raw RRDtool fetch output
|
|
* @param int $normalized_end_time Absolute requested end timestamp
|
|
* @param bool $show_unknown Preserve unknown values as 'U'
|
|
*
|
|
* @return array Parsed data source names, timestamps, effective step, and values
|
|
*/
|
|
function rrdtool_parse_fetch_output(string $output, int $normalized_end_time, bool $show_unknown = false) : array {
|
|
/** @var array{data_source_names?: list<string>, timestamp?: array{start_time?: int, end_time?: int, step?: int}, values?: array<int, array<int, numeric-string|'U'>>} $fetch_array */
|
|
$fetch_array = [];
|
|
$lines = explode("\n", $output);
|
|
$first = true;
|
|
$previous_timestamp = null;
|
|
$last_timestamp = null;
|
|
$effective_step = 0;
|
|
|
|
foreach ($lines as $line) {
|
|
$line = trim($line);
|
|
|
|
if ($first) {
|
|
if ($line == '') {
|
|
continue;
|
|
}
|
|
|
|
$names = preg_split('/\s+/', $line);
|
|
|
|
if ($names === false) {
|
|
return [];
|
|
}
|
|
|
|
$fetch_array['data_source_names'] = $names;
|
|
$first = false;
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($line == '' || !str_contains($line, ':')) {
|
|
continue;
|
|
}
|
|
|
|
[$raw_timestamp, $raw_data] = explode(':', $line, 2);
|
|
|
|
if (!is_numeric(trim($raw_timestamp))) {
|
|
continue;
|
|
}
|
|
|
|
$timestamp = (int) trim($raw_timestamp);
|
|
|
|
if ($previous_timestamp !== null) {
|
|
$observed_step = $timestamp - $previous_timestamp;
|
|
|
|
if ($observed_step > 0 && ($effective_step == 0 || $observed_step < $effective_step)) {
|
|
$effective_step = $observed_step;
|
|
}
|
|
}
|
|
|
|
if ($timestamp > $normalized_end_time) {
|
|
continue;
|
|
}
|
|
|
|
$previous_timestamp = $timestamp;
|
|
$last_timestamp = $timestamp;
|
|
$data = preg_split('/\s+/', trim($raw_data));
|
|
|
|
if ($data === false) {
|
|
continue;
|
|
}
|
|
|
|
if (!isset($fetch_array['timestamp']['start_time'])) {
|
|
$fetch_array['timestamp']['start_time'] = $timestamp;
|
|
}
|
|
|
|
foreach ($data as $index => $number) {
|
|
if (cacti_strtolower($number) == 'nan' || cacti_strtolower($number) == '-nan') {
|
|
if ($show_unknown) {
|
|
$fetch_array['values'][$index][$timestamp] = 'U';
|
|
}
|
|
} elseif (is_numeric($number)) {
|
|
$fetch_array['values'][$index][$timestamp] = $number;
|
|
} elseif ($show_unknown) {
|
|
$fetch_array['values'][$index][$timestamp] = 'U';
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($last_timestamp !== null) {
|
|
$fetch_array['timestamp']['end_time'] = $last_timestamp;
|
|
$fetch_array['timestamp']['step'] = $effective_step;
|
|
}
|
|
|
|
return $fetch_array;
|
|
}
|
|
|
|
/**
|
|
* rrdtool_function_fetch - given a data source, return all of its data in an array
|
|
*
|
|
* @param int $local_data_id The data source to fetch data for
|
|
* @param int $start_time The start time to use for the data calculation. this value can
|
|
* either be absolute (unix timestamp) or relative (to now)
|
|
* @param int $end_time The end time to use for the data calculation. this value can
|
|
* either be absolute (unix timestamp) or relative (to now)
|
|
* @param int $resolution The accuracy of the data measured in seconds
|
|
* @param bool $show_unknown Show unknown 'NAN' values in the output as 'U'
|
|
* @param string $rrdtool_file Don't force Cacti to calculate the file
|
|
* @param string $cf Specify the consolidation function to use
|
|
* @param mixed $rrdtool_pipe A pipe to an rrdtool command
|
|
*
|
|
* @return array An array containing all data in this data source broken down
|
|
* by each data source item. the maximum of all data source items is included in
|
|
* an item called 'nth_percentile_maximum'. The array will look as follows:
|
|
*
|
|
* $fetch_array['data_source_names'][0] = 'ds1'
|
|
* $fetch_array['data_source_names'][1] = 'ds2'
|
|
* $fetch_array['data_source_names'][2] = 'nth_percentile_maximum'
|
|
* $fetch_array['start_time'] = $timestamp;
|
|
* $fetch_array['end_time'] = $timestamp;
|
|
* $fetch_array['values'][$dsindex1][...] = $value;
|
|
* $fetch_array['values'][$dsindex2][...] = $value;
|
|
* $fetch_array['values'][$nth_index][...] = $value;
|
|
*
|
|
* Again, the 'nth_percentile_maximum' will have the maximum value amongst all the
|
|
* data sources for each set of data. So, if you have traffic_in and traffic_out,
|
|
* each member element in the array will have the maximum of traffic_in and traffic_out
|
|
* in it.
|
|
*/
|
|
function rrdtool_function_fetch(int $local_data_id, int $start_time, int $end_time, int $resolution = 0,
|
|
bool $show_unknown = false, string $rrdtool_file = '', string $cf = 'AVERAGE', mixed $rrdtool_pipe = null) : array {
|
|
include_once(CACTI_PATH_LIBRARY . '/boost.php');
|
|
|
|
// validate local data id
|
|
if (empty($local_data_id) && $rrdtool_file == '') {
|
|
return [];
|
|
}
|
|
|
|
$time = time();
|
|
$normalized_start_time = $start_time < 0 ? $time + $start_time : $start_time;
|
|
$normalized_end_time = $end_time < 0 ? $time + $end_time : $end_time;
|
|
|
|
// initialize fetch array
|
|
$fetch_array = [];
|
|
|
|
// check if we have been passed a file instead of local data source to look up
|
|
if ($rrdtool_file == '') {
|
|
$data_source_path = get_data_source_path($local_data_id, true);
|
|
} else {
|
|
$data_source_path = $rrdtool_file;
|
|
}
|
|
|
|
// Find the correct resolution
|
|
if ($resolution == 0) {
|
|
$resolution = rrdtool_function_get_resstep($local_data_id, $normalized_start_time, $normalized_end_time, 'res');
|
|
}
|
|
|
|
// update the rrdfile if performing a fetch
|
|
boost_fetch_cache_check($local_data_id, $rrdtool_pipe);
|
|
|
|
// rrdtool consolidation function is a fixed keyword set; never pass it through unchecked
|
|
if (!in_array($cf, ['AVERAGE', 'MIN', 'MAX', 'LAST'], true)) {
|
|
$cf = 'AVERAGE';
|
|
}
|
|
|
|
// build and run the rrdtool fetch command with all of our data
|
|
$cmd_line = 'fetch ' . cacti_escapeshellarg($data_source_path) . " $cf -s $normalized_start_time -e $normalized_end_time";
|
|
|
|
if ($resolution > 0) {
|
|
$cmd_line .= " -r $resolution";
|
|
}
|
|
|
|
$output = rrdtool_execute($cmd_line, false, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe);
|
|
|
|
if (!is_string($output)) {
|
|
return $fetch_array;
|
|
}
|
|
|
|
return rrdtool_parse_fetch_output($output, $normalized_end_time, $show_unknown);
|
|
}
|
|
|
|
function rrd_function_process_graph_options(int $graph_start, int $graph_end, array &$graph, array &$graph_data_array) : string {
|
|
global $image_types;
|
|
|
|
include(CACTI_PATH_INCLUDE . '/global_arrays.php');
|
|
|
|
// define some variables
|
|
$scale = '';
|
|
$rigid = '';
|
|
$unit_value = '';
|
|
$version = get_rrdtool_version();
|
|
$unit_exponent_value = '';
|
|
|
|
if ($graph['auto_scale'] == 'on') {
|
|
switch ($graph['auto_scale_opts']) {
|
|
case '1': // autoscale ignores lower, upper limit
|
|
$scale = '--alt-autoscale' . RRD_NL;
|
|
|
|
break;
|
|
case '2': // autoscale-max, accepts a given lower limit
|
|
$scale = '--alt-autoscale-max' . RRD_NL;
|
|
|
|
if (is_numeric($graph['lower_limit'])) {
|
|
$scale .= '--lower-limit=' . cacti_escapeshellarg((string) $graph['lower_limit']) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case '3': // autoscale-min, accepts a given upper limit
|
|
$scale = '--alt-autoscale-min' . RRD_NL;
|
|
|
|
if (is_numeric($graph['upper_limit'])) {
|
|
$scale .= '--upper-limit=' . cacti_escapeshellarg((string) $graph['upper_limit']) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case '4': // auto_scale with limits
|
|
$scale = '--alt-autoscale' . RRD_NL;
|
|
|
|
if (is_numeric($graph['upper_limit'])) {
|
|
$scale .= '--upper-limit=' . cacti_escapeshellarg((string) $graph['upper_limit']) . RRD_NL;
|
|
}
|
|
|
|
if (is_numeric($graph['lower_limit'])) {
|
|
$scale .= '--lower-limit=' . cacti_escapeshellarg((string) $graph['lower_limit']) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
}
|
|
} else {
|
|
if ($graph['upper_limit'] != '') {
|
|
$scale = '--upper-limit=' . cacti_escapeshellarg($graph['upper_limit']) . RRD_NL;
|
|
}
|
|
|
|
if ($graph['lower_limit'] != '') {
|
|
$scale .= '--lower-limit=' . cacti_escapeshellarg($graph['lower_limit']) . RRD_NL;
|
|
}
|
|
}
|
|
|
|
if ($graph['auto_scale_log'] == 'on') {
|
|
$scale .= '--logarithmic' . RRD_NL;
|
|
}
|
|
|
|
// --units=si only defined for logarithmic y-axis scaling, even if it doesn't hurt on linear graphs
|
|
if ($graph['scale_log_units'] == 'on' && $graph['auto_scale_log'] == 'on') {
|
|
$scale .= '--units=si' . RRD_NL;
|
|
}
|
|
|
|
if ($graph['auto_scale_rigid'] == 'on') {
|
|
$rigid = '--rigid' . RRD_NL;
|
|
}
|
|
|
|
if ($graph['unit_value'] != '') {
|
|
$unit_value = '--y-grid=' . cacti_escapeshellarg($graph['unit_value']) . RRD_NL;
|
|
}
|
|
|
|
if (preg_match('/^[0-9]+$/', $graph['unit_exponent_value'])) {
|
|
$unit_exponent_value = '--units-exponent=' . cacti_escapeshellarg($graph['unit_exponent_value']) . RRD_NL;
|
|
}
|
|
|
|
/*
|
|
* optionally you can specify and array that overrides some of the db's values, lets set
|
|
* that all up here
|
|
*/
|
|
|
|
// override: graph height (in pixels)
|
|
if (isset($graph_data_array['graph_height'])) {
|
|
$graph_height = $graph_data_array['graph_height'];
|
|
} else {
|
|
$graph_height = $graph['height'];
|
|
}
|
|
|
|
// override: graph width (in pixels)
|
|
if (isset($graph_data_array['graph_width'])) {
|
|
$graph_width = $graph_data_array['graph_width'];
|
|
} else {
|
|
$graph_width = $graph['width'];
|
|
}
|
|
|
|
// override: skip drawing the legend?
|
|
if (isset($graph_data_array['graph_nolegend'])) {
|
|
$graph_legend = '--no-legend' . RRD_NL;
|
|
} else {
|
|
$graph_legend = '';
|
|
}
|
|
|
|
// export options
|
|
if (isset($graph_data_array['export'])) {
|
|
$graph_opts = cacti_escapeshellarg((string) $graph_data_array['export_filename']) . RRD_NL;
|
|
} else {
|
|
if (empty($graph_data_array['output_filename'])) {
|
|
$graph_opts = '-' . RRD_NL;
|
|
} else {
|
|
$graph_opts = cacti_escapeshellarg((string) $graph_data_array['output_filename']) . RRD_NL;
|
|
}
|
|
}
|
|
|
|
if (isset($graph_data_array['image_format']) && $graph_data_array['image_format'] == 'png') {
|
|
$graph['image_format_id'] = 1;
|
|
}
|
|
|
|
// basic graph options
|
|
$graph_opts .=
|
|
'--imgformat=' . $image_types[$graph['image_format_id']] . RRD_NL .
|
|
'--start=' . $graph_start . RRD_NL .
|
|
'--end=' . $graph_end . RRD_NL;
|
|
|
|
$graph_opts .= '--pango-markup ' . RRD_NL;
|
|
|
|
if (read_config_option('rrdtool_watermark') == 'on') {
|
|
$graph_opts .= '--disable-rrdtool-tag ' . RRD_NL;
|
|
}
|
|
|
|
// activate --add-jsontime option for graphv only and RRDtool version 1.8.0 and above
|
|
if (isset($graph_data_array['graphv'])) {
|
|
$rrdversion = get_rrdtool_version();
|
|
|
|
if (cacti_version_compare($rrdversion, '1.8', '>=')) {
|
|
$graph_opts .= '--add-jsontime ' . RRD_NL;
|
|
}
|
|
}
|
|
|
|
foreach ($graph as $key => $value) {
|
|
if (is_string($value)) {
|
|
$value = rrdtool_resolve_graph_text($value, $graph);
|
|
}
|
|
|
|
switch($key) {
|
|
case 'title_cache':
|
|
if (!empty($value)) {
|
|
$graph_opts .= '--title=' . cacti_escapeshellarg(htmle($value)) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'alt_y_grid':
|
|
if ($value == CHECKED) {
|
|
$graph_opts .= '--alt-y-grid' . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'unit_value':
|
|
if (!empty($value)) {
|
|
$graph_opts .= '--y-grid=' . cacti_escapeshellarg($value) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'unit_exponent_value':
|
|
if (preg_match('/^[0-9]+$/', $value)) {
|
|
$graph_opts .= '--units-exponent=' . $value . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'height':
|
|
if (isset($graph_data_array['graph_height']) && preg_match('/^[0-9]+$/', $graph_data_array['graph_height'])) {
|
|
$graph_opts .= '--height=' . $graph_data_array['graph_height'] . RRD_NL;
|
|
} else {
|
|
$graph_opts .= '--height=' . $value . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'width':
|
|
if (isset($graph_data_array['graph_width']) && preg_match('/^[0-9]+$/', $graph_data_array['graph_width'])) {
|
|
$graph_opts .= '--width=' . $graph_data_array['graph_width'] . RRD_NL;
|
|
} else {
|
|
$graph_opts .= '--width=' . $value . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'graph_nolegend':
|
|
if (isset($graph_data_array['graph_nolegend'])) {
|
|
$graph_opts .= '--no-legend' . RRD_NL;
|
|
} else {
|
|
$graph_opts .= '';
|
|
}
|
|
|
|
break;
|
|
case 'base_value':
|
|
if ($value == 1000 || $value == 1024) {
|
|
$graph_opts .= '--base=' . $value . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'vertical_label':
|
|
if (!empty($value)) {
|
|
$graph_opts .= '--vertical-label=' . cacti_escapeshellarg(htmle($value)) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'slope_mode':
|
|
if ($value == CHECKED) {
|
|
$graph_opts .= '--slope-mode' . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'right_axis':
|
|
if (!empty($value)) {
|
|
$graph_opts .= '--right-axis ' . cacti_escapeshellarg($value) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'right_axis_label':
|
|
if (!empty($value)) {
|
|
$graph_opts .= '--right-axis-label ' . cacti_escapeshellarg($value) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'right_axis_format':
|
|
if (!empty($value)) {
|
|
$format = $graph['right_axis_format_text'];
|
|
$graph_opts .= '--right-axis-format ' . cacti_escapeshellarg(trim(str_replace('%s', '', $format))) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'left_axis_format':
|
|
if (!empty($value)) {
|
|
$format = $graph['left_axis_format_text'];
|
|
$graph_opts .= '--left-axis-format ' . cacti_escapeshellarg(trim(str_replace('%s', '', $format))) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'no_gridfit':
|
|
if ($value == CHECKED) {
|
|
$graph_opts .= '--no-gridfit' . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'unit_length':
|
|
if (!empty($value)) {
|
|
$graph_opts .= '--units-length ' . cacti_escapeshellarg($value) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'tab_width':
|
|
if (!empty($value)) {
|
|
$graph_opts .= '--tabwidth ' . cacti_escapeshellarg($value) . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'dynamic_labels':
|
|
if ($value == CHECKED) {
|
|
$graph_opts .= '--dynamic-labels' . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'force_rules_legend':
|
|
if ($value == CHECKED) {
|
|
$graph_opts .= '--force-rules-legend' . RRD_NL;
|
|
}
|
|
|
|
break;
|
|
case 'legend_position':
|
|
if (cacti_version_compare($version, '1.4', '>=')) {
|
|
if (!empty($value)) {
|
|
$graph_opts .= '--legend-position ' . cacti_escapeshellarg($value) . RRD_NL;
|
|
}
|
|
}
|
|
|
|
break;
|
|
case 'legend_direction':
|
|
if (cacti_version_compare($version, '1.4', '>=')) {
|
|
if (!empty($value)) {
|
|
$graph_opts .= '--legend-direction ' . cacti_escapeshellarg($value) . RRD_NL;
|
|
}
|
|
}
|
|
|
|
break;
|
|
case 'left_axis_formatter':
|
|
if (cacti_version_compare($version, '1.4', '>=')) {
|
|
if (!empty($value)) {
|
|
$graph_opts .= '--left-axis-formatter ' . cacti_escapeshellarg($value) . RRD_NL;
|
|
}
|
|
}
|
|
|
|
break;
|
|
case 'right_axis_formatter':
|
|
if (cacti_version_compare($version, '1.4', '>=')) {
|
|
if (!empty($value)) {
|
|
$graph_opts .= '--right-axis-formatter ' . cacti_escapeshellarg($value) . RRD_NL;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
$graph_opts .= "$rigid" . trim("$scale$unit_value$unit_exponent_value$graph_legend", "\n\r " . RRD_NL) . RRD_NL;
|
|
|
|
// add a date to the graph legend
|
|
$graph_opts .= rrdtool_function_format_graph_date($graph_data_array);
|
|
|
|
// process theme and font styling options
|
|
$graph_opts .= rrdtool_function_theme_font_options($graph_data_array);
|
|
|
|
/* NOTE: title, vertical-label and right-axis-label perform |query_*|
|
|
* substitution before cacti_escapeshellarg above, so a device-supplied
|
|
* value cannot break out of the quoted RRDtool argument. */
|
|
|
|
$watermark = str_replace("'", '"', read_config_option('graph_watermark'));
|
|
|
|
if ($watermark != '') {
|
|
$graph_opts .= '--watermark ' . cacti_escapeshellarg($watermark) . RRD_NL;
|
|
}
|
|
|
|
// if the user desires to hide RRDtools warker, set it
|
|
if (read_config_option('graph_watermark_rrd') != '') {
|
|
$graph_opts .= '--disable-rrdtool-tag' . RRD_NL;
|
|
}
|
|
|
|
return $graph_opts;
|
|
}
|
|
|
|
/**
|
|
* Resolve variables in one graph text value before the value is encoded for
|
|
* RRDtool or a process transport.
|
|
*
|
|
* Graph text must never expand monitoring credentials. Apart from leaking
|
|
* secrets into graph output and debug logs, inserting a replacement after
|
|
* quoting would allow the replacement to escape its argument boundary.
|
|
*
|
|
* @param string $value Unresolved graph text
|
|
* @param array $graph Graph context
|
|
* @param array $graph_item Optional graph-item context
|
|
* @param callable|null $resolver Test seam for the substitution provider
|
|
*
|
|
* @return string Resolved graph text with sensitive variables removed
|
|
*/
|
|
function rrdtool_resolve_graph_text(string $value, array $graph, array $graph_item = [], ?callable $resolver = null) : string {
|
|
$value = preg_replace(
|
|
'/\|host_snmp_(?:community|username|password|priv_passphrase|context|engine_id)\|/i',
|
|
'',
|
|
$value
|
|
) ?? '';
|
|
|
|
$resolver ??= 'rrd_substitute_host_query_data';
|
|
|
|
return (string) $resolver($value, $graph, $graph_item);
|
|
}
|
|
|
|
function rrdtool_function_graph(int $local_graph_id, mixed $rra_id, array $graph_data_array,
|
|
mixed $rrdtool_pipe = null, mixed &$xport_meta = [], int $user = 0) : mixed {
|
|
global $consolidation_functions, $graph_item_types, $encryption;
|
|
|
|
include_once(CACTI_PATH_LIBRARY . '/cdef.php');
|
|
include_once(CACTI_PATH_LIBRARY . '/vdef.php');
|
|
include_once(CACTI_PATH_LIBRARY . '/graph_variables.php');
|
|
include_once(CACTI_PATH_LIBRARY . '/boost.php');
|
|
include_once(CACTI_PATH_LIBRARY . '/xml.php');
|
|
include(CACTI_PATH_INCLUDE . '/global_arrays.php');
|
|
|
|
/* prevent command injection
|
|
* This function prepares an rrdtool graph statement to be executed by the web server.
|
|
* We have to take care, that the attacker does not insert shell code.
|
|
* As some rrdtool parameters accept "Cacti variables", we have to perform the
|
|
* variable substitution prior to vulnerability checks.
|
|
* We will enclose all parameters in quotes and substitute quotation marks within
|
|
* those parameters.
|
|
*/
|
|
|
|
/* before we do anything; make sure the user has permission to view this graph,
|
|
if not then get out */
|
|
if ($user > 0) {
|
|
if (!is_graph_allowed($local_graph_id, $user)) {
|
|
return 'GRAPH ACCESS DENIED';
|
|
}
|
|
}
|
|
|
|
if (getenv('LANG') == '') {
|
|
putenv('LANG=' . str_replace('-', '_', CACTI_LOCALE) . '.UTF-8');
|
|
}
|
|
|
|
/* several callers pass '' when no rra is selected; boost_graph_set_file()
|
|
expects int|null and throws a TypeError on a string. Only whole numbers
|
|
are archive ids; anything else falls back to best-fit selection */
|
|
$rra_id = (is_int($rra_id) || ctype_digit((string)$rra_id)) ? (int)$rra_id : null;
|
|
|
|
// check the purge the boost poller output cache, and check for a live image file if caching is enabled
|
|
$graph_data = boost_graph_cache_check($local_graph_id, $rra_id, $rrdtool_pipe, $graph_data_array, false);
|
|
|
|
if ($graph_data !== false) {
|
|
return $graph_data;
|
|
}
|
|
|
|
if (empty($graph_data_array['graph_start'])) {
|
|
$graph_data_array['graph_start'] = -86400;
|
|
}
|
|
|
|
if (empty($graph_data_array['graph_end'])) {
|
|
$main_last_run = intval(read_config_option('poller_lastrun_1'));
|
|
$now_time = time();
|
|
$default_delta = intval(read_config_option('poller_interval')) * -1;
|
|
|
|
if ($main_last_run > 0) {
|
|
$delta_time = $main_last_run - $now_time;
|
|
|
|
// DST time change detected
|
|
if ($delta_time > 0) {
|
|
$delta_time = $default_delta;
|
|
}
|
|
} else {
|
|
$delta_time = $default_delta;
|
|
}
|
|
|
|
$graph_data_array['graph_end'] = $delta_time;
|
|
}
|
|
|
|
$local_data_ids = array_rekey(
|
|
db_fetch_assoc_prepared('SELECT dtr.local_data_id
|
|
FROM graph_templates_item AS gti
|
|
INNER JOIN data_template_rrd AS dtr
|
|
ON gti.task_item_id = dtr.id
|
|
WHERE dtr.local_data_id > 0
|
|
AND gti.local_graph_id = ?',
|
|
[$local_graph_id]),
|
|
'local_data_id', 'local_data_id'
|
|
);
|
|
|
|
$ds_step = rrdtool_function_get_resstep($local_data_ids, $graph_data_array['graph_start'], $graph_data_array['graph_end'], 'step');
|
|
|
|
/* if no rra was specified, we need to figure out which one RRDtool will choose using
|
|
* "best-fit" resolution fit algorithm */
|
|
if (empty($rra_id)) {
|
|
if ($graph_data_array['graph_start'] == 0 || $graph_data_array['graph_end'] == 0) {
|
|
$rra['rows'] = 600;
|
|
$rra['steps'] = 1;
|
|
$rra['timespan'] = 86400;
|
|
} else {
|
|
// get a list of RRAs related to this graph
|
|
$rras = get_associated_rras($local_graph_id);
|
|
|
|
if (cacti_sizeof($rras)) {
|
|
foreach ($rras as $unchosen_rra) {
|
|
// the timespan specified in the RRA "timespan" field may not be accurate
|
|
$real_timespan = ($ds_step * $unchosen_rra['steps'] * $unchosen_rra['rows']);
|
|
|
|
// make sure the current start/end times fit within each RRA's timespan
|
|
if ($graph_data_array['graph_end'] - $graph_data_array['graph_start'] <= $real_timespan && time() - $graph_data_array['graph_start'] <= $real_timespan) {
|
|
// is this RRA better than the already chosen one?
|
|
if (isset($rra) && $unchosen_rra['steps'] < $rra['steps']) {
|
|
$rra = $unchosen_rra;
|
|
} elseif (!isset($rra)) {
|
|
$rra = $unchosen_rra;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isset($rra)) {
|
|
$rra['rows'] = 600;
|
|
$rra['steps'] = 1;
|
|
$rra['timespan'] = 86400;
|
|
}
|
|
}
|
|
} else {
|
|
$rra = db_fetch_row_prepared('SELECT
|
|
dspr.rows, dsp.step, dspr.steps
|
|
FROM data_source_profiles_rra AS dspr
|
|
INNER JOIN data_source_profiles AS dsp
|
|
ON dspr.data_source_profile_id=dsp.id
|
|
WHERE dspr.id = ?',
|
|
[$rra_id]
|
|
);
|
|
|
|
if (is_array($rra) && isset($rra['steps'])) {
|
|
$rra['timespan'] = $rra['rows'] * $rra['step'] * $rra['steps'];
|
|
} else {
|
|
$rra = [];
|
|
$rra['timespan'] = 86400;
|
|
$rra['steps'] = 1;
|
|
$rra['rows'] = 600;
|
|
}
|
|
}
|
|
|
|
if (!isset($graph_data_array['export_realtime']) && isset($rra['steps'])) {
|
|
$rra_seconds = max(1, (int) $ds_step * (int) $rra['steps']);
|
|
} else {
|
|
$rra_seconds = 5;
|
|
}
|
|
|
|
$graph = db_fetch_row_prepared('SELECT gl.id AS local_graph_id, gl.host_id,
|
|
gl.snmp_query_id, gl.snmp_index, gtg.title_cache, gtg.vertical_label,
|
|
gtg.slope_mode, gtg.auto_scale, gtg.auto_scale_opts, gtg.auto_scale_log,
|
|
gtg.scale_log_units, gtg.auto_scale_rigid, gtg.auto_padding, gtg.base_value,
|
|
gtg.upper_limit, gtg.lower_limit, gtg.height, gtg.width, gtg.image_format_id,
|
|
gtg.unit_value, gtg.unit_exponent_value, gtg.alt_y_grid,
|
|
gtg.right_axis, gtg.right_axis_label, gtg.right_axis_format, gtg.no_gridfit,
|
|
gtg.unit_length, gtg.tab_width, gtg.dynamic_labels, gtg.force_rules_legend,
|
|
gtg.legend_position, gtg.legend_direction, gtg.right_axis_formatter,
|
|
gtg.left_axis_format, gtg.left_axis_formatter,
|
|
right_gprint.gprint_text AS right_axis_format_text,
|
|
left_gprint.gprint_text AS left_axis_format_text
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id=gtg.local_graph_id
|
|
LEFT JOIN graph_templates_gprint AS right_gprint
|
|
ON right_gprint.id = gtg.right_axis_format
|
|
LEFT JOIN graph_templates_gprint AS left_gprint
|
|
ON left_gprint.id = gtg.left_axis_format
|
|
WHERE gtg.local_graph_id = ?',
|
|
[$local_graph_id]
|
|
);
|
|
|
|
// handle the case where the graph has been deleted
|
|
if (!cacti_sizeof($graph)) {
|
|
return false;
|
|
}
|
|
|
|
// lets make that sql query...
|
|
$graph_items = db_fetch_assoc_prepared('SELECT gti.id AS graph_templates_item_id,
|
|
gti.cdef_id, gti.vdef_id, gti.text_format, gti.value, gti.hard_return,
|
|
gti.consolidation_function_id, gti.graph_type_id, gtgp.gprint_text,
|
|
colors.hex, colors2.hex AS hex2, gti.alpha, gti.alpha2, gti.gradheight,
|
|
gti.line_width, gti.dashes, gti.shift,
|
|
gti.dash_offset, gti.textalign, dl.host_id AS data_source_host_id,
|
|
dl.snmp_query_id, dl.snmp_index, gti.legend,
|
|
dtr.id AS data_template_rrd_id, dl.id AS local_data_id,
|
|
dtr.rrd_minimum, dtr.rrd_maximum, dtr.data_source_name, dtr.local_data_template_rrd_id,
|
|
dtd.rrd_step
|
|
FROM graph_templates_item AS gti
|
|
LEFT JOIN data_template_rrd AS dtr
|
|
ON gti.task_item_id=dtr.id
|
|
LEFT JOIN data_template_data AS dtd
|
|
ON dtd.local_data_id = dtr.local_data_id
|
|
LEFT JOIN data_local AS dl
|
|
ON dl.id = dtr.local_data_id
|
|
LEFT JOIN colors
|
|
ON gti.color_id = colors.id
|
|
LEFT JOIN colors AS colors2
|
|
ON gti.color2_id = colors2.id
|
|
LEFT JOIN graph_templates_gprint AS gtgp
|
|
ON gti.gprint_id = gtgp.id
|
|
WHERE gti.local_graph_id = ?
|
|
ORDER BY gti.sequence',
|
|
[$local_graph_id]
|
|
);
|
|
|
|
// variables for use below
|
|
$graph_defs = '';
|
|
$txt_graph_items = '';
|
|
$pad_number = 0;
|
|
|
|
// override: graph start time
|
|
if ($graph_data_array['graph_start'] == 0) {
|
|
$graph_start = -($rra['timespan']);
|
|
} else {
|
|
$graph_start = $graph_data_array['graph_start'];
|
|
}
|
|
|
|
// override: graph end time
|
|
if ($graph_data_array['graph_end'] == 0) {
|
|
$graph_end = -($rra_seconds);
|
|
} else {
|
|
$graph_end = $graph_data_array['graph_end'];
|
|
}
|
|
|
|
// +++++++++++++++++++++++ GRAPH OPTIONS +++++++++++++++++++++++
|
|
|
|
if (!isset($graph_data_array['export_csv'])) {
|
|
$graph_opts = rrd_function_process_graph_options($graph_start, $graph_end, $graph, $graph_data_array);
|
|
} else {
|
|
// basic export options
|
|
$export_step = max(1, $rra_seconds);
|
|
$export_rows = (int) ceil(abs($graph_end - $graph_start) / $export_step) + 10;
|
|
$graph_opts = '--start=' . $graph_start . RRD_NL . '--end=' . $graph_end . RRD_NL . '--maxrows=' . max(10000, $export_rows) . RRD_NL;
|
|
}
|
|
|
|
// +++++++++++++++++++++++ LEGEND: MAGIC +++++++++++++++++++++++
|
|
|
|
$realtimeCachePath = read_config_option('realtime_cache_path');
|
|
$dateTimeFormat = read_config_option('graph_dateformat');
|
|
$cactiLastDate = read_config_option('date');
|
|
|
|
if (empty($cactiLastDate)) {
|
|
$cactiLastDate = date('Y-m-d H:i:s');
|
|
}
|
|
|
|
$dateTime = date($dateTimeFormat, strtotime($cactiLastDate));
|
|
|
|
// the following fields will be searched for graph variables
|
|
$variable_fields = [
|
|
'text_format' => [
|
|
'process_no_legend' => false
|
|
],
|
|
'value' => [
|
|
'process_no_legend' => true
|
|
],
|
|
'cdef_cache' => [
|
|
'process_no_legend' => true
|
|
],
|
|
'vdef_cache' => [
|
|
'process_no_legend' => true
|
|
]
|
|
];
|
|
|
|
$i = 0;
|
|
$j = 0;
|
|
$nth = 0;
|
|
$sum = 0;
|
|
$last_graph_cf = [];
|
|
$legends = [];
|
|
$defs = [];
|
|
$graph_variables = [];
|
|
$cf_ds_cache = [];
|
|
$cdef_cache = [];
|
|
$vdef_cache = [];
|
|
|
|
if (cacti_sizeof($graph_items)) {
|
|
// we need to add a new column 'cf_reference', so unless PHP 5 is used, this foreach syntax is required
|
|
foreach ($graph_items as $key => $graph_item) {
|
|
// mimic the old behavior: LINE[123], AREA and STACK items use the CF specified in the graph item
|
|
switch ($graph_item['graph_type_id']) {
|
|
case GRAPH_ITEM_TYPE_LINE1:
|
|
case GRAPH_ITEM_TYPE_LINE2:
|
|
case GRAPH_ITEM_TYPE_LINE3:
|
|
case GRAPH_ITEM_TYPE_LINESTACK:
|
|
case GRAPH_ITEM_TYPE_TIC:
|
|
case GRAPH_ITEM_TYPE_AREA:
|
|
case GRAPH_ITEM_TYPE_STACK:
|
|
$graph_cf = generate_graph_best_cf($graph_item['local_data_id'], $graph_item['consolidation_function_id']);
|
|
/* remember the last CF for this data source for use with GPRINT
|
|
* if e.g. an AREA/AVERAGE and a LINE/MAX is used
|
|
* we will have AVERAGE first and then MAX, depending on GPRINT sequence */
|
|
$last_graph_cf[$graph_item['data_source_name']][$graph_item['data_template_rrd_id']] = $graph_cf;
|
|
// remember this for second foreach loop
|
|
$graph_items[$key]['cf_reference'] = $graph_cf;
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_GPRINT:
|
|
/* ATTENTION!
|
|
* the 'CF' given on graph_item edit screen for GPRINT is indeed NOT a real 'CF',
|
|
* but an aggregation function
|
|
* see 'man rrdgraph_data' for the correct VDEF based notation
|
|
* so our task now is to 'guess' the very graph_item, this GPRINT is related to
|
|
* and to use that graph_item's CF */
|
|
if (isset($last_graph_cf[$graph_item['data_source_name']][$graph_item['data_template_rrd_id']])) {
|
|
$graph_cf = $last_graph_cf[$graph_item['data_source_name']][$graph_item['data_template_rrd_id']];
|
|
// remember this for second foreach loop
|
|
$graph_items[$key]['cf_reference'] = $graph_cf;
|
|
} else {
|
|
$graph_cf = generate_graph_best_cf($graph_item['local_data_id'], $graph_item['consolidation_function_id']);
|
|
// remember this for second foreach loop
|
|
$graph_items[$key]['cf_reference'] = $graph_cf;
|
|
}
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_GPRINT_AVERAGE:
|
|
case GRAPH_ITEM_TYPE_GPRINT_LAST:
|
|
case GRAPH_ITEM_TYPE_GPRINT_MAX:
|
|
case GRAPH_ITEM_TYPE_GPRINT_MIN:
|
|
$graph_cf = $graph_item['consolidation_function_id'];
|
|
$graph_items[$key]['cf_reference'] = $graph_cf;
|
|
|
|
break;
|
|
default:
|
|
// all other types are based on the best matching CF
|
|
$graph_cf = generate_graph_best_cf($graph_item['local_data_id'], $graph_item['consolidation_function_id']);
|
|
// remember this for second foreach loop
|
|
$graph_items[$key]['cf_reference'] = $graph_cf;
|
|
|
|
break;
|
|
}
|
|
|
|
$cf_ds_key = "{$graph_item['data_template_rrd_id']}:{$graph_cf}";
|
|
|
|
if ($graph_item['local_data_id'] > 0 && !isset($cf_ds_cache[$cf_ds_key])) {
|
|
// use a user-specified ds path if one is entered
|
|
if (isset($graph_data_array['export_realtime'])) {
|
|
$data_source_path = $realtimeCachePath . '/user_' . hash('sha256', session_id() ?: '') . '_' . $graph_item['local_data_id'] . '.rrd';
|
|
} else {
|
|
$data_source_path = get_data_source_path($graph_item['local_data_id'], true);
|
|
}
|
|
|
|
if (!rrdtool_file_exists($data_source_path, $rrdtool_pipe)) {
|
|
if (read_config_option('log_verbosity') >= POLLER_VERBOSITY_DEBUG || isset($graph_data_array['get_error'])) {
|
|
cacti_log("WARNING: RRD file '$data_source_path' does not exist", false, 'GRAPH');
|
|
}
|
|
|
|
if (isset($graph_data_array['export_csv'])) {
|
|
return false;
|
|
}
|
|
|
|
// Both get_error (STDERR output) and print_source (HTML debug view)
|
|
// are text-output modes; returning PNG bytes in either would produce
|
|
// garbage in the browser or CLI. Return a human-readable error instead.
|
|
if (isset($graph_data_array['get_error']) || isset($graph_data_array['print_source'])) {
|
|
return __('ERROR: RRD file does not exist: %s', $data_source_path);
|
|
}
|
|
|
|
return rrdtool_create_error_image(__('The Cacti Poller has not run yet.'));
|
|
}
|
|
|
|
// FOR WIN32: Escape all colon for drive letters (ex. D\:/path/to/rra)
|
|
$data_source_path = rrdtool_escape_string($data_source_path);
|
|
|
|
if ($data_source_path != '') {
|
|
/**
|
|
* NOTE: (Update) Data source DEF names are created using the graph_item_id; then passed
|
|
* to a function that matches the digits with letters. rrdtool likes letters instead
|
|
* of numbers in DEF names; especially with CDEFs. CDEFs are created
|
|
* the same way, except a 'cdef' is put on the beginning of the hash
|
|
*/
|
|
$def = generate_graph_def_name(intval($i));
|
|
$defs[] = $def;
|
|
|
|
$graph_defs .= "DEF:$def=" . cacti_escapeshellarg($data_source_path) . ':' . cacti_escapeshellarg($graph_item['data_source_name'], true) . ':' . $consolidation_functions[$graph_cf] . ':step=' . max(1, $rra_seconds) . RRD_NL;
|
|
|
|
$cf_ds_cache[$cf_ds_key] = $i;
|
|
|
|
if ($graph_item['legend'] == '') {
|
|
$legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]] = ucwords(str_replace('_', ' ', $graph_item['data_source_name'])) . ' (' . $consolidation_functions[$graph_cf] . ')';
|
|
} else {
|
|
$legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]] = $graph_item['legend'];
|
|
}
|
|
|
|
$i++;
|
|
}
|
|
}
|
|
|
|
// cache cdef value here to support data query variables in the cdef string
|
|
if (empty($graph_item['cdef_id'])) {
|
|
$graph_item['cdef_cache'] = '';
|
|
$graph_items[$j]['cdef_cache'] = '';
|
|
} else {
|
|
$cdef = get_cdef($graph_item['cdef_id']);
|
|
|
|
$graph_item['cdef_cache'] = $cdef;
|
|
$graph_items[$j]['cdef_cache'] = $cdef;
|
|
}
|
|
|
|
// cache vdef value here
|
|
if (empty($graph_item['vdef_id'])) {
|
|
$graph_item['vdef_cache'] = '';
|
|
$graph_items[$j]['vdef_cache'] = '';
|
|
} else {
|
|
$vdef = get_vdef($graph_item['vdef_id']);
|
|
|
|
$graph_item['vdef_cache'] = $vdef;
|
|
$graph_items[$j]['vdef_cache'] = $vdef;
|
|
}
|
|
|
|
// +++++++++++++++++++++++ LEGEND: TEXT SUBSTITUTION (<>) +++++++++++++++++++++++
|
|
|
|
// note the current item_id for easy access
|
|
$graph_item_id = $graph_item['graph_templates_item_id'];
|
|
|
|
/* loop through each field that we want to substitute values for:
|
|
currently: text format and value */
|
|
foreach ($variable_fields as $field_name => $field_array) {
|
|
// certain fields do not require values when the legend is not to be shown
|
|
if ($field_array['process_no_legend'] == false && isset($graph_data_array['graph_nolegend'])) {
|
|
continue;
|
|
}
|
|
|
|
$graph_variables[$field_name][$graph_item_id] = $graph_item[$field_name];
|
|
|
|
$search = [];
|
|
$replace = [];
|
|
|
|
// date/time substitution
|
|
if (strstr($graph_variables[$field_name][$graph_item_id], '|date_time|')) {
|
|
$search[] = '|date_time|';
|
|
$replace[] = $dateTime;
|
|
}
|
|
|
|
// data source title substitution
|
|
if (strstr($graph_variables[$field_name][$graph_item_id], '|data_source_title|')) {
|
|
$search[] = '|data_source_title|';
|
|
$replace[] = get_data_source_title($graph_item['local_data_id']);
|
|
}
|
|
|
|
// data query variables
|
|
$graph_variables[$field_name][$graph_item_id] = rrdtool_resolve_graph_text($graph_variables[$field_name][$graph_item_id], $graph, $graph_item);
|
|
|
|
// Nth percentile
|
|
if (preg_match_all('/\|([0-9]{1,2}):(bits|bytes):(\d):(current|total|max|total_peak|all_max_current|all_max_peak|aggregate_max|aggregate_sum|aggregate_sum_peak|aggregate_current|aggregate_current_peak|aggregate_peak|aggregate):(\d)?\|/', $graph_variables[$field_name][$graph_item_id], $matches, PREG_SET_ORDER)) {
|
|
foreach ($matches as $match) {
|
|
$search[] = $match[0];
|
|
$value = variable_nth_percentile($match, $graph, $graph_item, $graph_items, $graph_start, $graph_end, $rra_seconds);
|
|
$replace[] = $value;
|
|
|
|
if ($field_name == 'value') {
|
|
$xport_meta['NthPercentile'][$nth]['format'] = $match[0];
|
|
$xport_meta['NthPercentile'][$nth]['value'] = str_replace($match[0], $value, $graph_variables[$field_name][$graph_item_id]);
|
|
$nth++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// bandwidth summation
|
|
if (preg_match_all('/\|sum:(\d|auto):(current|total|atomic):(\d):(\d+|auto)\|/', $graph_variables[$field_name][$graph_item_id], $matches, PREG_SET_ORDER)) {
|
|
foreach ($matches as $match) {
|
|
$search[] = $match[0];
|
|
$value = variable_bandwidth_summation($match, $graph, $graph_item, $graph_items, $graph_start, $graph_end, $rra['steps'], $ds_step);
|
|
$replace[] = $value;
|
|
|
|
if ($field_name == 'text_format') {
|
|
$xport_meta['Summation'][$sum]['format'] = $match[0];
|
|
$xport_meta['Summation'][$sum]['value'] = str_replace($match[0], $value, $graph_variables[$field_name][$graph_item_id]);
|
|
$sum++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cacti_count($search)) {
|
|
$graph_variables[$field_name][$graph_item_id] = str_replace($search, $replace, $graph_variables[$field_name][$graph_item_id]);
|
|
}
|
|
}
|
|
|
|
/* if we are not displaying a legend there is no point in us even processing the auto padding,
|
|
text format stuff. */
|
|
if (!isset($graph_data_array['graph_nolegend'])) {
|
|
// set hard return variable if selected (\n)
|
|
if ($graph_item['hard_return'] == 'on') {
|
|
$hardreturn[$graph_item_id] = '\\n';
|
|
} else {
|
|
$hardreturn[$graph_item_id] = '';
|
|
}
|
|
}
|
|
|
|
$j++;
|
|
}
|
|
}
|
|
|
|
// +++++++++++++++++++++++ LEGEND: AUTO PADDING (<>) +++++++++++++++++++++++
|
|
if (cacti_sizeof($graph_items)) {
|
|
/* we need to add a new column 'cf_reference', so unless PHP 5 is used, this foreach
|
|
* syntax is required
|
|
*/
|
|
foreach ($graph_items as $graph_item) {
|
|
// note the current item_id for easy access
|
|
$graph_item_id = $graph_item['graph_templates_item_id'];
|
|
|
|
/* if we are not displaying a legend there is no point in us even processing the
|
|
* auto padding, text format stuff.
|
|
*/
|
|
if (!isset($graph_data_array['graph_nolegend'])) {
|
|
/* PADDING: remember this is not perfect! its main use is for the basic graph setup of:
|
|
* AREA - GPRINT-CURRENT - GPRINT-AVERAGE - GPRINT-MAXIMUM \n
|
|
* of course it can be used in other situations, however may not work as intended.
|
|
* If you have any additions to this small piece of code, feel free to send them to me.
|
|
*/
|
|
if ($graph['auto_padding'] == 'on') {
|
|
// only applies to AREA, STACK and LINEs
|
|
if (preg_match('/(AREA|STACK|LINE[123])/', $graph_item_types[$graph_item['graph_type_id']])) {
|
|
$text_format_length = mb_strlen(trim($graph_variables['text_format'][$graph_item_id]), 'UTF-8');
|
|
|
|
if ($text_format_length > $pad_number) {
|
|
$pad_number = $text_format_length;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// +++++++++++++++++++++++ LEGEND: AUTO PADDING (<>) +++++++++++++++++++++++
|
|
|
|
// +++++++++++++++++++++++ GRAPH ITEMS: CDEF +++++++++++++++++++++++
|
|
|
|
$i = 0;
|
|
|
|
// hack for rrdtool 1.2.x support
|
|
$graph_item_stack_type = '';
|
|
$graph_vdefs = [];
|
|
$stacked_columns = [];
|
|
|
|
if (cacti_sizeof($graph_items)) {
|
|
foreach ($graph_items as $graph_item) {
|
|
// Restore the $cf_reference from the first pass of the graph items
|
|
$graph_cf = $graph_item['cf_reference'];
|
|
$cf_ds_key = "{$graph_item['data_template_rrd_id']}:{$graph_cf}";
|
|
|
|
/**
|
|
* first we need to check if there is a DEF for the current data source/cf combination. if so,
|
|
* we will use that
|
|
*/
|
|
if (isset($cf_ds_cache[$cf_ds_key])) {
|
|
$cf_id = $graph_item['consolidation_function_id'];
|
|
} else {
|
|
/* if there is not a DEF defined for the current data source/cf combination, then we will have to
|
|
improvise. choose the first available cf in the following order: AVERAGE, MAX, MIN, LAST */
|
|
if (isset($cf_ds_cache["{$graph_item['data_template_rrd_id']}:1"])) {
|
|
$cf_id = 1; // CF: AVERAGE
|
|
} elseif (isset($cf_ds_cache["{$graph_item['data_template_rrd_id']}:3"])) {
|
|
$cf_id = 3; // CF: MAX
|
|
} elseif (isset($cf_ds_cache["{$graph_item['data_template_rrd_id']}:2"])) {
|
|
$cf_id = 2; // CF: MIN
|
|
} elseif (isset($cf_ds_cache["{$graph_item['data_template_rrd_id']}:4"])) {
|
|
$cf_id = 4; // CF: LAST
|
|
} else {
|
|
$cf_id = 1; // CF: AVERAGE
|
|
}
|
|
}
|
|
|
|
// +++++++++++++++++++++++ GRAPH ITEMS: CDEF START +++++++++++++++++++++++
|
|
|
|
/**
|
|
* make cdef string here; a note about CDEFs in cacti. A CDEF is neither unique to a
|
|
* data source of global cdef, but is unique when those two variables combine.
|
|
*/
|
|
$cdef_graph_defs = '';
|
|
$special_def = 1;
|
|
|
|
$cdef_cache_key = "{$graph_item['cdef_id']}:{$graph_item['data_template_rrd_id']}:{$cf_id}";
|
|
|
|
if ($graph_item['cdef_id'] > 0 && !isset($cdef_cache[$cdef_cache_key])) {
|
|
/** @var string $cdef_string */
|
|
$cdef_string = $graph_variables['cdef_cache'][$graph_item['graph_templates_item_id']];
|
|
$magic_item = [];
|
|
$already_seen = [];
|
|
$sources_seen = [];
|
|
|
|
$count_all_ds_dups = 0;
|
|
$count_all_ds_nodups = 0;
|
|
$count_similar_ds_dups = 0;
|
|
$count_similar_ds_nodups = 0;
|
|
|
|
// if any of those magic variables are requested ...
|
|
if (preg_match('/(ALL_DATA_SOURCES_(NO)?DUPS|SIMILAR_DATA_SOURCES_(NO)?DUPS)/', $cdef_string) ||
|
|
preg_match('/(COUNT_ALL_DS_(NO)?DUPS|COUNT_SIMILAR_DS_(NO)?DUPS)/', $cdef_string)) {
|
|
// now walk through each case to initialize array
|
|
if (preg_match('/ALL_DATA_SOURCES_DUPS/', $cdef_string)) {
|
|
$magic_item['ALL_DATA_SOURCES_DUPS'] = '';
|
|
}
|
|
|
|
if (preg_match('/ALL_DATA_SOURCES_NODUPS/', $cdef_string)) {
|
|
$magic_item['ALL_DATA_SOURCES_NODUPS'] = '';
|
|
}
|
|
|
|
if (preg_match('/SIMILAR_DATA_SOURCES_DUPS/', $cdef_string)) {
|
|
$magic_item['SIMILAR_DATA_SOURCES_DUPS'] = '';
|
|
}
|
|
|
|
if (preg_match('/SIMILAR_DATA_SOURCES_NODUPS/', $cdef_string)) {
|
|
$magic_item['SIMILAR_DATA_SOURCES_NODUPS'] = '';
|
|
}
|
|
|
|
if (preg_match('/COUNT_ALL_DS_DUPS/', $cdef_string)) {
|
|
$magic_item['COUNT_ALL_DS_DUPS'] = '';
|
|
}
|
|
|
|
if (preg_match('/COUNT_ALL_DS_NODUPS/', $cdef_string)) {
|
|
$magic_item['COUNT_ALL_DS_NODUPS'] = '';
|
|
}
|
|
|
|
if (preg_match('/COUNT_SIMILAR_DS_DUPS/', $cdef_string)) {
|
|
$magic_item['COUNT_SIMILAR_DS_DUPS'] = '';
|
|
}
|
|
|
|
if (preg_match('/COUNT_SIMILAR_DS_NODUPS/', $cdef_string)) {
|
|
$magic_item['COUNT_SIMILAR_DS_NODUPS'] = '';
|
|
}
|
|
|
|
// loop over all graph items
|
|
foreach ($graph_items as $gi_check) {
|
|
// only work on graph items, omit GRPINTs, COMMENTs and stuff
|
|
if ((preg_match('/(AREA|STACK|LINE[123])/', $graph_item_types[$gi_check['graph_type_id']])) && (!empty($gi_check['data_template_rrd_id']))) {
|
|
// if the user screws up CF settings, PHP will generate warnings if left unchecked
|
|
$magic_cf_ds_key = "{$gi_check['data_template_rrd_id']}:{$cf_id}";
|
|
|
|
// matching consolidation function?
|
|
if (isset($cf_ds_cache[$magic_cf_ds_key])) {
|
|
$def_name = generate_graph_def_name($cf_ds_cache[$magic_cf_ds_key]);
|
|
|
|
// do we need ALL_DATA_SOURCES_DUPS?
|
|
if (isset($magic_item['ALL_DATA_SOURCES_DUPS'])) {
|
|
$magic_item['ALL_DATA_SOURCES_DUPS'] .= ($count_all_ds_dups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,$def_name,$def_name,UN,0,$def_name,IF,IF"; // convert unknowns to '0' first
|
|
}
|
|
|
|
// do we need COUNT_ALL_DS_DUPS?
|
|
if (isset($magic_item['COUNT_ALL_DS_DUPS'])) {
|
|
$magic_item['COUNT_ALL_DS_DUPS'] .= ($count_all_ds_dups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,1,$def_name,UN,0,1,IF,IF"; // convert unknowns to '0' first
|
|
}
|
|
|
|
$count_all_ds_dups++;
|
|
|
|
// check if this item also qualifies for NODUPS
|
|
if (!isset($already_seen[$def_name])) {
|
|
if (isset($magic_item['ALL_DATA_SOURCES_NODUPS'])) {
|
|
$magic_item['ALL_DATA_SOURCES_NODUPS'] .= ($count_all_ds_nodups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,$def_name,$def_name,UN,0,$def_name,IF,IF"; // convert unknowns to '0' first
|
|
}
|
|
|
|
if (isset($magic_item['COUNT_ALL_DS_NODUPS'])) {
|
|
$magic_item['COUNT_ALL_DS_NODUPS'] .= ($count_all_ds_nodups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,1,$def_name,UN,0,1,IF,IF"; // convert unknowns to '0' first
|
|
}
|
|
|
|
$count_all_ds_nodups++;
|
|
$already_seen[$def_name] = true;
|
|
}
|
|
|
|
// check for SIMILAR data sources
|
|
if ($graph_item['data_source_name'] == $gi_check['data_source_name']) {
|
|
// do we need SIMILAR_DATA_SOURCES_DUPS?
|
|
if (isset($magic_item['SIMILAR_DATA_SOURCES_DUPS']) && ($graph_item['data_source_name'] == $gi_check['data_source_name'])) {
|
|
$magic_item['SIMILAR_DATA_SOURCES_DUPS'] .= ($count_similar_ds_dups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,$def_name,$def_name,UN,0,$def_name,IF,IF"; // convert unknowns to '0' first
|
|
}
|
|
|
|
// do we need COUNT_SIMILAR_DS_DUPS?
|
|
if (isset($magic_item['COUNT_SIMILAR_DS_DUPS']) && ($graph_item['data_source_name'] == $gi_check['data_source_name'])) {
|
|
$magic_item['COUNT_SIMILAR_DS_DUPS'] .= ($count_similar_ds_dups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,1,$def_name,UN,0,1,IF,IF"; // convert unknowns to '0' first
|
|
}
|
|
|
|
$count_similar_ds_dups++;
|
|
|
|
// check if this item also qualifies for NODUPS
|
|
if (!isset($sources_seen[$gi_check['data_template_rrd_id']])) {
|
|
if (isset($magic_item['SIMILAR_DATA_SOURCES_NODUPS'])) {
|
|
$magic_item['SIMILAR_DATA_SOURCES_NODUPS'] .= ($count_similar_ds_nodups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,$def_name,$def_name,UN,0,$def_name,IF,IF"; // convert unknowns to '0' first
|
|
}
|
|
|
|
if (isset($magic_item['COUNT_SIMILAR_DS_NODUPS']) && ($graph_item['data_source_name'] == $gi_check['data_source_name'])) {
|
|
$magic_item['COUNT_SIMILAR_DS_NODUPS'] .= ($count_similar_ds_nodups == 0 ? '' : ',') . 'TIME,' . (time() - $rra_seconds) . ",GT,1,$def_name,UN,0,1,IF,IF"; // convert unknowns to '0' first
|
|
}
|
|
|
|
$count_similar_ds_nodups++;
|
|
$sources_seen[$gi_check['data_template_rrd_id']] = true;
|
|
}
|
|
} // SIMILAR data sources
|
|
} // matching consolidation function?
|
|
} // only work on graph items, omit GRPINTs, COMMENTs and stuff
|
|
} // loop over all graph items
|
|
|
|
/* if there is only one item to total, don't even bother with the summation.
|
|
* Otherwise cdef=a,b,c,+,+ is fine. */
|
|
if ($count_all_ds_dups > 1 && isset($magic_item['ALL_DATA_SOURCES_DUPS'])) {
|
|
$magic_item['ALL_DATA_SOURCES_DUPS'] .= str_repeat(',+', ($count_all_ds_dups - 2)) . ',+';
|
|
}
|
|
|
|
if ($count_all_ds_nodups > 1 && isset($magic_item['ALL_DATA_SOURCES_NODUPS'])) {
|
|
$magic_item['ALL_DATA_SOURCES_NODUPS'] .= str_repeat(',+', ($count_all_ds_nodups - 2)) . ',+';
|
|
}
|
|
|
|
if ($count_similar_ds_dups > 1 && isset($magic_item['SIMILAR_DATA_SOURCES_DUPS'])) {
|
|
$magic_item['SIMILAR_DATA_SOURCES_DUPS'] .= str_repeat(',+', ($count_similar_ds_dups - 2)) . ',+';
|
|
}
|
|
|
|
if ($count_similar_ds_nodups > 1 && isset($magic_item['SIMILAR_DATA_SOURCES_NODUPS'])) {
|
|
$magic_item['SIMILAR_DATA_SOURCES_NODUPS'] .= str_repeat(',+', ($count_similar_ds_nodups - 2)) . ',+';
|
|
}
|
|
|
|
if ($count_all_ds_dups > 1 && isset($magic_item['COUNT_ALL_DS_DUPS'])) {
|
|
$magic_item['COUNT_ALL_DS_DUPS'] .= str_repeat(',+', ($count_all_ds_dups - 2)) . ',+';
|
|
}
|
|
|
|
if ($count_all_ds_nodups > 1 && isset($magic_item['COUNT_ALL_DS_NODUPS'])) {
|
|
$magic_item['COUNT_ALL_DS_NODUPS'] .= str_repeat(',+', ($count_all_ds_nodups - 2)) . ',+';
|
|
}
|
|
|
|
if ($count_similar_ds_dups > 1 && isset($magic_item['COUNT_SIMILAR_DS_DUPS'])) {
|
|
$magic_item['COUNT_SIMILAR_DS_DUPS'] .= str_repeat(',+', ($count_similar_ds_dups - 2)) . ',+';
|
|
}
|
|
|
|
if ($count_similar_ds_nodups > 1 && isset($magic_item['COUNT_SIMILAR_DS_NODUPS'])) {
|
|
$magic_item['COUNT_SIMILAR_DS_NODUPS'] .= str_repeat(',+', ($count_similar_ds_nodups - 2)) . ',+';
|
|
}
|
|
}
|
|
|
|
// allow automatic rate calculations on raw gauge data
|
|
if (isset($graph_item['local_data_id'])) {
|
|
$polling_interval = $graph_item['rrd_step'];
|
|
} else {
|
|
$polling_interval = read_config_option('poller_interval');
|
|
}
|
|
|
|
$cdef_string = str_replace('CURRENT_DATA_SOURCE_PI', $polling_interval, $cdef_string);
|
|
|
|
$cdef_string = str_replace('CURRENT_DATA_SOURCE', generate_graph_def_name($cf_ds_cache[$cf_ds_key] ?? 0), $cdef_string);
|
|
|
|
// allow automatic rate calculations on raw gauge data
|
|
$cdef_string = str_replace('ALL_DATA_SOURCES_DUPS_PI', $polling_interval, $cdef_string);
|
|
|
|
// ALL|SIMILAR_DATA_SOURCES(NO)?DUPS are to be replaced here
|
|
if (isset($magic_item['ALL_DATA_SOURCES_DUPS'])) {
|
|
$cdef_string = str_replace('ALL_DATA_SOURCES_DUPS', $magic_item['ALL_DATA_SOURCES_DUPS'], $cdef_string);
|
|
}
|
|
|
|
// allow automatic rate calculations on raw gauge data
|
|
$cdef_string = str_replace('ALL_DATA_SOURCES_NODUPS_PI', $polling_interval, $cdef_string);
|
|
|
|
if (isset($magic_item['ALL_DATA_SOURCES_NODUPS'])) {
|
|
$cdef_string = str_replace('ALL_DATA_SOURCES_NODUPS', $magic_item['ALL_DATA_SOURCES_NODUPS'], $cdef_string);
|
|
}
|
|
|
|
// allow automatic rate calculations on raw gauge data
|
|
$cdef_string = str_replace('SIMILAR_DATA_SOURCES_DUPS_PI', $polling_interval, $cdef_string);
|
|
|
|
if (isset($magic_item['SIMILAR_DATA_SOURCES_DUPS'])) {
|
|
$cdef_string = str_replace('SIMILAR_DATA_SOURCES_DUPS', $magic_item['SIMILAR_DATA_SOURCES_DUPS'], $cdef_string);
|
|
}
|
|
|
|
$cdef_string = str_replace('SIMILAR_DATA_SOURCES_NODUPS_PI', $polling_interval, $cdef_string);
|
|
|
|
if (isset($magic_item['SIMILAR_DATA_SOURCES_NODUPS'])) {
|
|
$cdef_string = str_replace('SIMILAR_DATA_SOURCES_NODUPS', $magic_item['SIMILAR_DATA_SOURCES_NODUPS'], $cdef_string);
|
|
}
|
|
|
|
// COUNT_ALL|SIMILAR_DATA_SOURCES(NO)?DUPS are to be replaced here
|
|
if (isset($magic_item['COUNT_ALL_DS_DUPS'])) {
|
|
$cdef_string = str_replace('COUNT_ALL_DS_DUPS', $magic_item['COUNT_ALL_DS_DUPS'], $cdef_string);
|
|
}
|
|
|
|
if (isset($magic_item['COUNT_ALL_DS_NODUPS'])) {
|
|
$cdef_string = str_replace('COUNT_ALL_DS_NODUPS', $magic_item['COUNT_ALL_DS_NODUPS'], $cdef_string);
|
|
}
|
|
|
|
if (isset($magic_item['COUNT_SIMILAR_DS_DUPS'])) {
|
|
$cdef_string = str_replace('COUNT_SIMILAR_DS_DUPS', $magic_item['COUNT_SIMILAR_DS_DUPS'], $cdef_string);
|
|
}
|
|
|
|
if (isset($magic_item['COUNT_SIMILAR_DS_NODUPS'])) {
|
|
$cdef_string = str_replace('COUNT_SIMILAR_DS_NODUPS', $magic_item['COUNT_SIMILAR_DS_NODUPS'], $cdef_string);
|
|
}
|
|
|
|
// data source item variables
|
|
$cdef_string = str_replace('CURRENT_DS_MINIMUM_VALUE', (empty($graph_item['rrd_minimum']) ? '0' : $graph_item['rrd_minimum']), $cdef_string);
|
|
$cdef_string = str_replace('CURRENT_DS_MAXIMUM_VALUE', (empty($graph_item['rrd_maximum']) ? '0' : $graph_item['rrd_maximum']), $cdef_string);
|
|
$cdef_string = str_replace('CURRENT_GRAPH_MINIMUM_VALUE', (empty($graph['lower_limit']) ? '0' : $graph['lower_limit']), $cdef_string);
|
|
$cdef_string = str_replace('CURRENT_GRAPH_MAXIMUM_VALUE', (empty($graph['upper_limit']) ? '0' : $graph['upper_limit']), $cdef_string);
|
|
|
|
// special data source types for prediction/trending
|
|
$def_name = generate_graph_def_name($special_def);
|
|
|
|
if (preg_match('/CURRENT_DS_LSLSLOPE/', $cdef_string)) {
|
|
$vdef_name = 'vdef' . $def_name . 'slope';
|
|
|
|
if (!isset($graph_vdefs[$vdef_name])) {
|
|
$graph_vdefs[$vdef_name] = "$vdef_name=$def_name,LSLSLOPE";
|
|
}
|
|
|
|
$cdef_string = str_replace('CURRENT_DS_LSLSLOPE', $vdef_name, $cdef_string);
|
|
|
|
$special_def++;
|
|
} elseif (preg_match('/CURRENT_DS_LSLINT/', $cdef_string)) {
|
|
$vdef_name = 'vdef' . $def_name . 'int';
|
|
|
|
if (!isset($graph_vdefs[$vdef_name])) {
|
|
$graph_vdefs[$vdef_name] = "$vdef_name=$def_name,LSLINT";
|
|
}
|
|
|
|
$cdef_string = str_replace('CURRENT_DS_LSLINT', $vdef_name, $cdef_string);
|
|
|
|
$special_def++;
|
|
} elseif (preg_match('/CURRENT_DS_LSLCORREL/', $cdef_string)) {
|
|
$vdef_name = 'vdef' . $def_name . 'correl';
|
|
|
|
if (!isset($graph_vdefs[$vdef_name])) {
|
|
$graph_vdefs[$vdef_name] = "$vdef_name=$def_name,LSLCORREL";
|
|
}
|
|
|
|
$cdef_string = str_replace('CURRENT_DS_LSLCORREL', $vdef_name, $cdef_string);
|
|
|
|
$special_def++;
|
|
} elseif (preg_match('/CURRENT_DS_LSLFUNCTION/', $cdef_string)) {
|
|
$vdefslope_name = 'vdef' . $def_name . 'slope';
|
|
$vdefint_name = 'vdef' . $def_name . 'int';
|
|
|
|
if (!isset($graph_vdefs[$vdefslope_name])) {
|
|
$graph_vdefs[$vdefslope_name] = "$vdefslope_name=$def_name,LSLSLOPE";
|
|
}
|
|
|
|
if (!isset($graph_vdefs[$vdefint_name])) {
|
|
$graph_vdefs[$vdefint_name] = "$vdefint_name=$def_name,LSLINT";
|
|
}
|
|
|
|
$cdef_string = str_replace('CURRENT_DS_LSLFUNCTION', "$def_name,POP,COUNT,$vdefslope_name,*,$vdefint_name,+" , $cdef_string);
|
|
|
|
$special_def++;
|
|
}
|
|
|
|
// generate the custom prediction vdef string
|
|
foreach ($graph_vdefs as $vdef_name => $vdef_def) {
|
|
if ($vdef_def != '') {
|
|
$graph_defs .= "VDEF:$vdef_def" . RRD_NL;
|
|
}
|
|
|
|
$graph_vdefs[$vdef_name] = '';
|
|
}
|
|
|
|
if ((str_contains($cdef_string, '|query_ifHighSpeed|')) ||
|
|
(str_contains($cdef_string, '|query_ifSpeed|'))) {
|
|
$local_data = db_fetch_row_prepared('SELECT *
|
|
FROM data_local
|
|
WHERE id = ?',
|
|
[$graph_item['local_data_id']]);
|
|
|
|
$speed = is_array($local_data) ? rrdtool_function_interface_speed($local_data) : '0';
|
|
|
|
$cdef_string = str_replace(['|query_ifHighSpeed|', '|query_ifSpeed|'], [$speed, $speed], $cdef_string);
|
|
}
|
|
|
|
// replace query variables in cdefs
|
|
$cdef_string = rrdtool_resolve_graph_text($cdef_string, $graph, $graph_item);
|
|
|
|
// aggregate graphs can produce an empty RPN expression for GPRINT items
|
|
// whose consolidation function does not match the data source; skip them
|
|
// rather than emitting a bare "CDEF:cdefX=" which rrdtool rejects.
|
|
if ($cdef_string === '') {
|
|
cacti_log('Empty CDEF string for graph ' . $graph['local_graph_id'] . '; skipping.', true, 'RRD', POLLER_VERBOSITY_DEBUG);
|
|
|
|
continue;
|
|
}
|
|
|
|
// make the initial 'virtual' cdef name: 'cdef' + [a,b,c,d...]
|
|
$cdef_graph_defs .= 'CDEF:cdef' . generate_graph_def_name(intval($i)) . '=';
|
|
// prohibit command injection and provide platform specific quoting
|
|
$cdef_graph_defs .= cacti_escapeshellarg(sanitize_cdef($cdef_string), true);
|
|
$cdef_graph_defs .= " \\\n";
|
|
|
|
// the CDEF cache is so we do not create duplicate CDEF's on a graph
|
|
$cdef_cache[$cdef_cache_key] = $i;
|
|
}
|
|
|
|
// add the cdef string to the end of the def string
|
|
$graph_defs .= $cdef_graph_defs;
|
|
|
|
// +++++++++++++++++++++++ GRAPH ITEMS: CDEFs END +++++++++++++++++++++++
|
|
|
|
// +++++++++++++++++++++++ GRAPH ITEMS: VDEFs START +++++++++++++++++++++++
|
|
|
|
$vdef_graph_defs = '';
|
|
$vdef_cache_key = "{$graph_item['vdef_id']}:{$graph_item['cdef_id']}:{$graph_item['data_template_rrd_id']}:{$cf_id}";
|
|
|
|
if ($graph_item['vdef_id'] > 0 && !isset($vdef_cache[$vdef_cache_key])) {
|
|
$vdef_string = $graph_variables['vdef_cache'][$graph_item['graph_templates_item_id']];
|
|
|
|
// do we refer to a CDEF within this VDEF?
|
|
if ($graph_item['cdef_id'] > 0) {
|
|
// 'calculated' VDEF: use (cached) CDEF as base, only way to get calculations into VDEFs
|
|
$vdef_string = 'cdef' . str_replace('CURRENT_DATA_SOURCE', generate_graph_def_name($cdef_cache[$cdef_cache_key]), $vdef_string);
|
|
} else {
|
|
// 'pure' VDEF: use DEF as base
|
|
$vdef_string = str_replace('CURRENT_DATA_SOURCE', generate_graph_def_name($cf_ds_cache[$cf_ds_key]), $vdef_string);
|
|
}
|
|
|
|
// TODO: It would be possible to refer to a CDEF, but that's all. So ALL_DATA_SOURCES_NODUPS and stuff can't be used directly!
|
|
// $vdef_string = str_replace('ALL_DATA_SOURCES_NODUPS', $magic_item['ALL_DATA_SOURCES_NODUPS'], $vdef_string);
|
|
// $vdef_string = str_replace('ALL_DATA_SOURCES_DUPS', $magic_item['ALL_DATA_SOURCES_DUPS'], $vdef_string);
|
|
// $vdef_string = str_replace('SIMILAR_DATA_SOURCES_NODUPS', $magic_item['SIMILAR_DATA_SOURCES_NODUPS'], $vdef_string);
|
|
// $vdef_string = str_replace('SIMILAR_DATA_SOURCES_DUPS', $magic_item['SIMILAR_DATA_SOURCES_DUPS'], $vdef_string);
|
|
|
|
// make the initial 'virtual' vdef name
|
|
$vdef_graph_defs .= 'VDEF:vdef' . generate_graph_def_name($i) . '=';
|
|
$vdef_graph_defs .= cacti_escapeshellarg(sanitize_cdef($vdef_string));
|
|
$vdef_graph_defs .= " \\\n";
|
|
|
|
/* the VDEF cache is so we do not create duplicate VDEFs on a graph,
|
|
* but take info account, that same VDEF may use different CDEFs
|
|
* so index over VDEF_ID, CDEF_ID per DATA_TEMPLATE_RRD_ID, lvm */
|
|
$vdef_cache[$vdef_cache_key] = $i;
|
|
}
|
|
|
|
// add the cdef string to the end of the def string
|
|
$graph_defs .= $vdef_graph_defs;
|
|
|
|
// +++++++++++++++++++++++ GRAPH ITEMS: VDEFs END +++++++++++++++++++++++
|
|
|
|
// note the current item_id for easy access
|
|
$graph_item_id = $graph_item['graph_templates_item_id'];
|
|
|
|
/* we put this in a variable so it can be manipulated before mainly used
|
|
if we want to skip it, like below */
|
|
$current_graph_item_type = $graph_item_types[$graph_item['graph_type_id']];
|
|
|
|
/**
|
|
* If this graph item has a data source... get a DEF name for it, or the cdef if that applies
|
|
* to this graph item
|
|
*/
|
|
if ($graph_item['cdef_id'] == 0) {
|
|
if (isset($cf_ds_cache[$cf_ds_key])) {
|
|
$data_source_name = generate_graph_def_name($cf_ds_cache[$cf_ds_key]);
|
|
} else {
|
|
$data_source_name = '';
|
|
}
|
|
} else {
|
|
$data_source_name = 'cdef' . generate_graph_def_name($cdef_cache[$cdef_cache_key]);
|
|
}
|
|
|
|
/**
|
|
* If this graph item has a data source... get a DEF name for it, or the vdef if that applies
|
|
* to this graph item .
|
|
*/
|
|
if ($graph_item['vdef_id'] > 0 && isset($vdef_cache[$vdef_cache_key])) {
|
|
$data_source_name = 'vdef' . generate_graph_def_name($vdef_cache[$vdef_cache_key]);
|
|
}
|
|
|
|
// to make things easier... if there is no text format set; set blank text
|
|
if (!isset($graph_variables['text_format'][$graph_item_id])) {
|
|
$graph_variables['text_format'][$graph_item_id] = '';
|
|
}
|
|
|
|
if (!isset($hardreturn[$graph_item_id])) {
|
|
$hardreturn[$graph_item_id] = '';
|
|
}
|
|
|
|
// +++++++++++++++++++++++ GRAPH ITEMS +++++++++++++++++++++++
|
|
|
|
/* most of the calculations have been done above. now we have for print everything out
|
|
in an RRDtool-friendly fashion */
|
|
|
|
$need_rrd_nl = true;
|
|
|
|
// initialize color support
|
|
$graph_item_color_code = '';
|
|
|
|
if (!empty($graph_item['hex'])) {
|
|
$graph_item_color_code = '#' . $graph_item['hex'];
|
|
$graph_item_color_code .= rrd_graph_item_alpha($graph_item['alpha']);
|
|
}
|
|
|
|
$rrdversion = get_rrdtool_version();
|
|
|
|
if (cacti_version_compare($rrdversion, '1.8', '>=')) {
|
|
if (!empty($graph_item['hex2']) && ($graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_AREA || $graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_STACK)) {
|
|
$graph_item_color_code2 = '#' . $graph_item['hex2'];
|
|
$graph_item_color_code2 .= rrd_graph_item_alpha($graph_item['alpha2']);
|
|
} else {
|
|
$graph_item_color_code2 = '';
|
|
}
|
|
} else {
|
|
$graph_item_color_code2 = '';
|
|
}
|
|
|
|
// initialize dash support
|
|
$dash = '';
|
|
|
|
if ($graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_LINE1 ||
|
|
$graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_LINE2 ||
|
|
$graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_LINE3 ||
|
|
$graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_LINESTACK ||
|
|
$graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_HRULE ||
|
|
$graph_item['graph_type_id'] == GRAPH_ITEM_TYPE_VRULE) {
|
|
$dashes = rrd_graph_item_number_list($graph_item['dashes']);
|
|
|
|
if ($dashes !== '') {
|
|
$dash .= ':dashes=' . $dashes;
|
|
}
|
|
|
|
$dash_offset = strip_alpha($graph_item['dash_offset']);
|
|
|
|
if (!empty($dash_offset)) {
|
|
$dash .= ':dash-offset=' . $dash_offset;
|
|
}
|
|
}
|
|
|
|
if (!isset($graph_data_array['export_csv'])) {
|
|
switch($graph_item['graph_type_id']) {
|
|
case GRAPH_ITEM_TYPE_COMMENT:
|
|
if (!isset($graph_data_array['graph_nolegend'])) {
|
|
$comments = [];
|
|
|
|
$comment_arg = rrdtool_resolve_graph_text($graph_variables['text_format'][$graph_item_id], $graph, $graph_item);
|
|
|
|
// Check for a wrapping comment
|
|
$max = read_config_option('max_title_length') - 20;
|
|
|
|
if (strlen($comment_arg) > $max) {
|
|
$comments = explode("\n", wordwrap($comment_arg, $max));
|
|
} else {
|
|
$comments[] = $comment_arg;
|
|
}
|
|
|
|
foreach ($comments as $comment) {
|
|
// next, compute the argument of the COMMENT statement and perform injection counter measures
|
|
if (trim($comment) == '') { // an empty COMMENT must be treated with care
|
|
$comment = cacti_escapeshellarg(' ' . $hardreturn[$graph_item_id]);
|
|
} else {
|
|
$comment = cacti_escapeshellarg(rrdtool_escape_string(htmle($comment)) . $hardreturn[$graph_item_id]);
|
|
}
|
|
|
|
// create rrdtool specific command line
|
|
$txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $comment . ' ';
|
|
}
|
|
}
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_TEXTALIGN:
|
|
if (!isset($graph_data_array['graph_nolegend'])) {
|
|
$textalign = rrd_graph_item_textalign($graph_item['textalign']);
|
|
|
|
if ($textalign !== '') {
|
|
$txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $textalign;
|
|
}
|
|
}
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_GPRINT:
|
|
$text_format = rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id]), false);
|
|
|
|
if ($graph_item['vdef_id'] == '0') {
|
|
$txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $data_source_name . ':' . $consolidation_functions[$graph_item['consolidation_function_id']] . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' ';
|
|
} else {
|
|
$txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $data_source_name . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' ';
|
|
}
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_GPRINT_AVERAGE:
|
|
if (!isset($graph_data_array['graph_nolegend'])) {
|
|
$text_format = rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id]));
|
|
|
|
if ($graph_item['vdef_id'] == '0') {
|
|
$txt_graph_items .= 'GPRINT:' . $data_source_name . ':AVERAGE:' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' ';
|
|
} else {
|
|
$txt_graph_items .= 'GPRINT:' . $data_source_name . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' ';
|
|
}
|
|
}
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_GPRINT_LAST:
|
|
if (!isset($graph_data_array['graph_nolegend'])) {
|
|
$text_format = rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id]));
|
|
|
|
if ($graph_item['vdef_id'] == '0') {
|
|
$txt_graph_items .= 'GPRINT:' . $data_source_name . ':LAST:' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' ';
|
|
} else {
|
|
$txt_graph_items .= 'GPRINT:' . $data_source_name . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' ';
|
|
}
|
|
}
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_GPRINT_MAX:
|
|
if (!isset($graph_data_array['graph_nolegend'])) {
|
|
$text_format = rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id]));
|
|
|
|
if ($graph_item['vdef_id'] == '0') {
|
|
$txt_graph_items .= 'GPRINT:' . $data_source_name . ':MAX:' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' ';
|
|
} else {
|
|
$txt_graph_items .= 'GPRINT:' . $data_source_name . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' ';
|
|
}
|
|
}
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_GPRINT_MIN:
|
|
if (!isset($graph_data_array['graph_nolegend'])) {
|
|
$text_format = rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id]));
|
|
|
|
if ($graph_item['vdef_id'] == '0') {
|
|
$txt_graph_items .= 'GPRINT:' . $data_source_name . ':MIN:' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' ';
|
|
} else {
|
|
$txt_graph_items .= 'GPRINT:' . $data_source_name . ':' . cacti_escapeshellarg($text_format . $graph_item['gprint_text'] . $hardreturn[$graph_item_id]) . ' ';
|
|
}
|
|
}
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_AREA:
|
|
if ($graph_variables['text_format'][$graph_item_id] != '') {
|
|
$text_format = rrdtool_escape_string(htmle(str_pad($graph_variables['text_format'][$graph_item_id], $pad_number)));
|
|
} elseif (isset($graph_data_array['graph_nolegend'])) {
|
|
if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) {
|
|
$text_format = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]];
|
|
} else {
|
|
$text_format = '';
|
|
}
|
|
} else {
|
|
$text_format = '';
|
|
}
|
|
|
|
if (read_config_option('enable_rrdtool_gradient_support') == 'on') {
|
|
// End color is a 40% (0.4) darkened (negative number) version of the original color
|
|
$end_color = colourBrightness('#' . $graph_item['hex'], -0.4);
|
|
$item_alpha = rrd_graph_item_alpha($graph_item['alpha']);
|
|
$txt_graph_items .= gradient($data_source_name, $graph_item_color_code, $end_color . $item_alpha, cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]), 20, false, $item_alpha);
|
|
} else {
|
|
$txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $data_source_name . $graph_item_color_code . $graph_item_color_code2 . ':' . cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]);
|
|
|
|
if ($graph_item_color_code2 != '') {
|
|
$gradheight = strip_alpha($graph_item['gradheight']);
|
|
|
|
if ($gradheight !== false) {
|
|
$txt_graph_items .= ':gradheight=' . $gradheight;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($graph_item['shift'] == CHECKED && abs($graph_item['value']) > 0) {
|
|
// create a SHIFT statement
|
|
$shift_value = strip_alpha($graph_item['value']);
|
|
|
|
if ($shift_value !== false) {
|
|
$txt_graph_items .= RRD_NL . 'SHIFT:' . $data_source_name . ':' . $shift_value;
|
|
}
|
|
}
|
|
|
|
$text_format = trim(preg_replace('/[^a-z0-9 _()]/i', '', $text_format));
|
|
$xport_meta['legend'][$text_format] = $graph_item_color_code;
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_STACK:
|
|
if ($graph_variables['text_format'][$graph_item_id] != '') {
|
|
$text_format = rrdtool_escape_string(htmle(str_pad($graph_variables['text_format'][$graph_item_id], $pad_number)));
|
|
} elseif (isset($graph_data_array['graph_nolegend'])) {
|
|
if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) {
|
|
$text_format = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]];
|
|
} else {
|
|
$text_format = '';
|
|
}
|
|
} else {
|
|
$text_format = '';
|
|
}
|
|
|
|
$txt_graph_items .= 'AREA:' . $data_source_name . $graph_item_color_code . $graph_item_color_code2 . ':' . cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]) . ':STACK';
|
|
|
|
if ($graph_item_color_code2 != '') {
|
|
$gradheight = strip_alpha($graph_item['gradheight']);
|
|
|
|
if ($gradheight !== false) {
|
|
$txt_graph_items .= ':gradheight=' . $gradheight;
|
|
}
|
|
}
|
|
|
|
if ($graph_item['shift'] == CHECKED && $graph_item['value'] > 0) { // create a SHIFT statement
|
|
$shift_value = strip_alpha($graph_item['value']);
|
|
|
|
if ($shift_value !== false) {
|
|
$txt_graph_items .= RRD_NL . 'SHIFT:' . $data_source_name . ':' . $shift_value;
|
|
}
|
|
}
|
|
|
|
$text_format = trim(preg_replace('/[^a-z0-9 _()]/i', '', $text_format));
|
|
$xport_meta['legend'][$text_format] = $graph_item_color_code;
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_LINE1:
|
|
case GRAPH_ITEM_TYPE_LINE2:
|
|
case GRAPH_ITEM_TYPE_LINE3:
|
|
if ($graph_variables['text_format'][$graph_item_id] != '') {
|
|
$text_format = rrdtool_escape_string(htmle(str_pad($graph_variables['text_format'][$graph_item_id], $pad_number)));
|
|
} elseif (isset($graph_data_array['graph_nolegend'])) {
|
|
if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) {
|
|
$text_format = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]];
|
|
} else {
|
|
$text_format = '';
|
|
}
|
|
} else {
|
|
$text_format = '';
|
|
}
|
|
|
|
$txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $data_source_name . $graph_item_color_code . ':' . cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]) . $dash;
|
|
|
|
if ($graph_item['shift'] == CHECKED && $graph_item['value'] > 0) { // create a SHIFT statement
|
|
$shift_value = strip_alpha($graph_item['value']);
|
|
|
|
if ($shift_value !== false) {
|
|
$txt_graph_items .= RRD_NL . 'SHIFT:' . $data_source_name . ':' . $shift_value;
|
|
}
|
|
}
|
|
|
|
$text_format = trim(preg_replace('/[^a-z0-9 _()]/i', '', $text_format));
|
|
$xport_meta['legend'][$text_format] = $graph_item_color_code;
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_LINESTACK:
|
|
if ($graph_variables['text_format'][$graph_item_id] != '') {
|
|
$text_format = rrdtool_escape_string(htmle(str_pad($graph_variables['text_format'][$graph_item_id], $pad_number)));
|
|
} elseif (isset($graph_data_array['graph_nolegend'])) {
|
|
if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) {
|
|
$text_format = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]];
|
|
} else {
|
|
$text_format = '';
|
|
}
|
|
} else {
|
|
$text_format = '';
|
|
}
|
|
|
|
$line_width = strip_alpha($graph_item['line_width']);
|
|
$txt_graph_items .= 'LINE' . ($line_width === false ? '1' : $line_width) . ':' . $data_source_name . $graph_item_color_code . ':' . cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]) . ':STACK' . $dash;
|
|
|
|
if ($graph_item['shift'] == CHECKED && $graph_item['value'] > 0) { // create a SHIFT statement
|
|
$shift_value = strip_alpha($graph_item['value']);
|
|
|
|
if ($shift_value !== false) {
|
|
$txt_graph_items .= RRD_NL . 'SHIFT:' . $data_source_name . ':' . $shift_value;
|
|
}
|
|
}
|
|
|
|
$text_format = trim(preg_replace('/[^a-z0-9 _()]/i', '', $text_format));
|
|
$xport_meta['legend'][$text_format] = $graph_item_color_code;
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_TIC:
|
|
$tick_fraction = strip_alpha($graph_item['value']);
|
|
$_fraction = (empty($graph_item['graph_type_id']) || $tick_fraction === false ? '' : (':' . $tick_fraction));
|
|
$_legend = ':' . cacti_escapeshellarg(rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id])) . $hardreturn[$graph_item_id]);
|
|
$txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $data_source_name . $graph_item_color_code . $_fraction . $_legend;
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_HRULE:
|
|
// perform variable substitution; if this does not return a number, rrdtool will FAIL!
|
|
$substitute = strip_alpha(rrdtool_resolve_graph_text($graph_variables['value'][$graph_item_id], $graph, $graph_item));
|
|
|
|
$text_format = rrdtool_escape_string(htmle(rrdtool_resolve_graph_text($graph_variables['text_format'][$graph_item_id], $graph, $graph_item)));
|
|
|
|
// don't break rrdtool if the strip_alpha() returns false
|
|
if ($substitute !== false) {
|
|
$graph_variables['value'][$graph_item_id] = $substitute;
|
|
} else {
|
|
$graph_variables['value'][$graph_item_id] = '0';
|
|
}
|
|
|
|
$txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $graph_variables['value'][$graph_item_id] . $graph_item_color_code . ':' . cacti_escapeshellarg($text_format . $hardreturn[$graph_item_id]) . '' . $dash;
|
|
|
|
break;
|
|
case GRAPH_ITEM_TYPE_VRULE:
|
|
if (substr_count($graph_item['value'], ':')) {
|
|
$value_array = explode(':', $graph_item['value']);
|
|
|
|
if ($value_array[0] < 0) {
|
|
$value = date('U') - (-3600 * (int) $value_array[0]) - 60 * (int) $value_array[1];
|
|
} else {
|
|
$mktime = mktime((int) $value_array[0], (int) $value_array[1], 0);
|
|
$value = date('U', $mktime !== false ? $mktime : 0);
|
|
}
|
|
|
|
$txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $value . $graph_item_color_code . ':' . cacti_escapeshellarg(rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id])) . $hardreturn[$graph_item_id]) . $dash;
|
|
} elseif (is_numeric($graph_item['value'])) {
|
|
$value = $graph_item['value'];
|
|
|
|
$txt_graph_items .= $graph_item_types[$graph_item['graph_type_id']] . ':' . $value . $graph_item_color_code . ':' . cacti_escapeshellarg(rrdtool_escape_string(htmle($graph_variables['text_format'][$graph_item_id])) . $hardreturn[$graph_item_id]) . $dash;
|
|
}
|
|
|
|
break;
|
|
default:
|
|
$need_rrd_nl = false;
|
|
}
|
|
} else {
|
|
/* rrdtool xport only accepts DEF/CDEF references; a VDEF backed
|
|
* item would emit XPORT:vdefNN and fail the whole command */
|
|
if ($graph_item['vdef_id'] == 0 && preg_match('/^(AREA|AREA:STACK|LINE[123]|STACK)$/', $graph_item_types[$graph_item['graph_type_id']])) {
|
|
$legend_name = '';
|
|
|
|
// give all export items a name
|
|
if (trim($graph_variables['text_format'][$graph_item_id]) == '') {
|
|
if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) {
|
|
$legend_name = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]];
|
|
}
|
|
} else {
|
|
if (isset($legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]])) {
|
|
$legend_name = $legends[$graph_item['local_data_id']][$graph_item['data_source_name']][$consolidation_functions[$graph_cf]];
|
|
} else {
|
|
$legend_name = $graph_variables['text_format'][$graph_item_id];
|
|
}
|
|
}
|
|
|
|
if ($legend_name == '') {
|
|
$legend_name = 'col' . $j . '-' . $data_source_name;
|
|
}
|
|
|
|
$stacked_columns[$legend_name] = ($graph_item_types[$graph_item['graph_type_id']] == 'STACK') ? 1 : 0;
|
|
$j++;
|
|
|
|
$txt_graph_items .= 'XPORT:' . cacti_escapeshellarg($data_source_name) . ':' . str_replace(':', '', cacti_escapeshellarg($legend_name));
|
|
} else {
|
|
$need_rrd_nl = false;
|
|
}
|
|
}
|
|
|
|
$i++;
|
|
|
|
if (($i < cacti_sizeof($graph_items)) && ($need_rrd_nl)) {
|
|
$txt_graph_items .= RRD_NL;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isset($graph_data_array['export_csv']) || $graph_data_array['export_csv'] != true) {
|
|
$graph_array = api_plugin_hook_function('rrd_graph_graph_options', ['graph_opts' => $graph_opts, 'graph_defs' => $graph_defs, 'txt_graph_items' => $txt_graph_items, 'graph_id' => $local_graph_id, 'start' => $graph_start, 'end' => $graph_end, 'defs' => $defs]);
|
|
|
|
$graph_array = add_unknown_data($graph_array, $xport_meta);
|
|
$graph_array = add_business_hours($graph_array, $xport_meta);
|
|
|
|
if (!empty($graph_array)) {
|
|
$graph_defs = $graph_array['graph_defs'];
|
|
$txt_graph_items = $graph_array['txt_graph_items'];
|
|
$graph_opts = $graph_array['graph_opts'];
|
|
$defs = $graph_array['defs'];
|
|
}
|
|
|
|
// either print out the source or pass the source onto rrdtool to get us a nice PNG
|
|
if (isset($graph_data_array['print_source'])) {
|
|
if (isset($graph_data_array['graphv'])) {
|
|
$graph = 'graphv';
|
|
} else {
|
|
$graph = 'graph';
|
|
}
|
|
|
|
$source_command_line = read_config_option('path_rrdtool') . " $graph " . $graph_opts . $graph_defs . $txt_graph_items;
|
|
$source_command_line_lengths = strlen(str_replace("\\\n", ' ', $source_command_line));
|
|
|
|
print '<pre>' . wordwrap(htmle($source_command_line), 160, PHP_EOL, true) . '</pre>';
|
|
print '<span class="textInfo">' . 'RRDtool Command lengths = ' . $source_command_line_lengths . ' characters.</span><br>';
|
|
|
|
if (CACTI_SERVER_OS == 'win32' && $source_command_line_lengths > 8191) {
|
|
print '<pre>' . 'Warning: The Cacti OS is Windows system, RRDtool Command lengths should not exceed 8191 characters.' . '</pre>';
|
|
}
|
|
} else {
|
|
if (isset($graph_data_array['graphv'])) {
|
|
$graph = 'graphv';
|
|
} else {
|
|
$graph = 'graph';
|
|
}
|
|
|
|
if (isset($graph_data_array['get_error'])) {
|
|
return rrdtool_execute("graph $graph_opts$graph_defs$txt_graph_items", false, RRDTOOL_OUTPUT_STDERR);
|
|
}
|
|
|
|
if (isset($graph_data_array['export'])) {
|
|
rrdtool_execute("graph $graph_opts$graph_defs$txt_graph_items", false, RRDTOOL_OUTPUT_NULL, $rrdtool_pipe);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (isset($graph_data_array['export_realtime'])) {
|
|
$output_flag = RRDTOOL_OUTPUT_GRAPH_DATA;
|
|
$output = rrdtool_execute("graph $graph_opts$graph_defs$txt_graph_items", false, $output_flag, $rrdtool_pipe);
|
|
|
|
if (!is_string($output) || !rrdtool_atomic_write(
|
|
(string) $graph_data_array['export_realtime'],
|
|
$output,
|
|
(string) read_config_option('realtime_cache_path')
|
|
)) {
|
|
cacti_log('ERROR: Unable to write realtime graph output safely.', false, 'RRDTOOL');
|
|
|
|
return false;
|
|
}
|
|
|
|
return $output;
|
|
} else {
|
|
$graph_data_array = boost_prep_graph_array($graph_data_array);
|
|
|
|
if (!isset($graph_data_array['output_flag'])) {
|
|
$output_flag = RRDTOOL_OUTPUT_GRAPH_DATA;
|
|
} else {
|
|
$output_flag = $graph_data_array['output_flag'];
|
|
}
|
|
|
|
$output = rrdtool_execute("$graph $graph_opts$graph_defs$txt_graph_items", false, $output_flag, $rrdtool_pipe);
|
|
|
|
boost_graph_set_file($output, $local_graph_id, $rra_id);
|
|
|
|
return $output;
|
|
}
|
|
}
|
|
} else {
|
|
$output_flag = RRDTOOL_OUTPUT_STDOUT;
|
|
|
|
$xport_array = rrdxport2array(rrdtool_execute("xport $graph_opts$graph_defs$txt_graph_items", false, $output_flag, $rrdtool_pipe));
|
|
|
|
if (!isset($xport_array['meta'])) {
|
|
cacti_log('WARNING: RRDtool xport returned no valid data for Local Graph ID ' . $local_graph_id, false, 'EXPORT');
|
|
|
|
return [];
|
|
}
|
|
|
|
// add host and graph information
|
|
$xport_array['meta']['stacked_columns'] = $stacked_columns;
|
|
$xport_array['meta']['title_cache'] = is_array($graph) ? $graph['title_cache'] : '';
|
|
$xport_array['meta']['vertical_label'] = is_array($graph) ? $graph['vertical_label'] : '';
|
|
$xport_array['meta']['local_graph_id'] = $local_graph_id;
|
|
$xport_array['meta']['host_id'] = is_array($graph) ? $graph['host_id'] : 0;
|
|
|
|
return $xport_array;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Atomically write generated RRDtool output beneath an approved directory.
|
|
*
|
|
* @param string $path Destination file
|
|
* @param string $contents Complete generated output
|
|
* @param string $allowed_root Configured directory that owns the destination
|
|
*
|
|
* @return bool Whether the complete file was safely replaced
|
|
*/
|
|
function rrdtool_atomic_write(string $path, string $contents, string $allowed_root) : bool {
|
|
$root = realpath($allowed_root);
|
|
$parent = realpath(dirname($path));
|
|
|
|
if ($root === false || $parent === false) {
|
|
return false;
|
|
}
|
|
|
|
$rootPrefix = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
|
|
|
if ($parent !== $root && !str_starts_with($parent . DIRECTORY_SEPARATOR, $rootPrefix)) {
|
|
return false;
|
|
}
|
|
|
|
$temporary = tempnam($parent, '.cacti-rrd-');
|
|
|
|
if ($temporary === false) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$written = file_put_contents($temporary, $contents, LOCK_EX);
|
|
|
|
if ($written !== strlen($contents) || !chmod($temporary, 0644)) {
|
|
return false;
|
|
}
|
|
|
|
return rename($temporary, $path);
|
|
} finally {
|
|
if (is_file($temporary)) {
|
|
unlink($temporary);
|
|
}
|
|
}
|
|
}
|
|
|
|
function rrdtool_escape_string(string $text, bool $ignore_percent = true) : string {
|
|
if ($ignore_percent) {
|
|
return str_replace(['"', ':'], ['\"', '\:'], $text);
|
|
} else {
|
|
return str_replace(['"', ':', '%'], ['\"', '\:', '%%'], $text);
|
|
}
|
|
}
|
|
|
|
function rrdtool_function_xport(int $local_graph_id, int $rra_id, array $xport_data_array, array &$xport_meta, int $user = 0) : mixed {
|
|
return rrdtool_function_graph($local_graph_id, $rra_id, $xport_data_array, null, $xport_meta, $user);
|
|
}
|
|
|
|
function rrdtool_function_format_graph_date(array &$graph_data_array) : string {
|
|
global $datechar;
|
|
|
|
$graph_legend = '';
|
|
// setup date format
|
|
$date_fmt = read_user_setting('default_date_format',read_config_option('default_date_format'));
|
|
$dateCharSetting = read_user_setting('default_datechar',read_config_option('default_datechar'));
|
|
$datecharacter = $datechar[$dateCharSetting];
|
|
|
|
switch ($date_fmt) {
|
|
case GD_MO_D_Y:
|
|
$graph_date = 'm' . $datecharacter . 'd' . $datecharacter . 'Y H:i:s';
|
|
|
|
break;
|
|
case GD_MN_D_Y:
|
|
$graph_date = 'M' . $datecharacter . 'd' . $datecharacter . 'Y H:i:s';
|
|
|
|
break;
|
|
case GD_D_MO_Y:
|
|
$graph_date = 'd' . $datecharacter . 'm' . $datecharacter . 'Y H:i:s';
|
|
|
|
break;
|
|
case GD_D_MN_Y:
|
|
$graph_date = 'd' . $datecharacter . 'M' . $datecharacter . 'Y H:i:s';
|
|
|
|
break;
|
|
case GD_Y_MO_D:
|
|
$graph_date = 'Y' . $datecharacter . 'm' . $datecharacter . 'd H:i:s';
|
|
|
|
break;
|
|
case GD_Y_MN_D:
|
|
$graph_date = 'Y' . $datecharacter . 'M' . $datecharacter . 'd H:i:s';
|
|
|
|
break;
|
|
default:
|
|
$graph_date = 'Y' . $datecharacter . 'M' . $datecharacter . 'd H:i:s';
|
|
|
|
break;
|
|
}
|
|
|
|
if (CACTI_WEB) {
|
|
cacti_time_zone_set();
|
|
}
|
|
|
|
// display the timespan for zoomed graphs
|
|
if ((isset($graph_data_array['graph_start'])) && (isset($graph_data_array['graph_end']))) {
|
|
if (($graph_data_array['graph_start'] < 0) && ($graph_data_array['graph_end'] < 0)) {
|
|
$graph_legend = 'COMMENT:"From ' . str_replace(':', '\:', date($graph_date, time() + $graph_data_array['graph_start'])) . ' To ' . str_replace(':', '\:', date($graph_date, time() + $graph_data_array['graph_end'])) . '\\c"' . RRD_NL . 'COMMENT:" \\n"' . RRD_NL;
|
|
} elseif (($graph_data_array['graph_start'] >= 0) && ($graph_data_array['graph_end'] >= 0)) {
|
|
$graph_legend = 'COMMENT:"From ' . str_replace(':', '\:', date($graph_date, $graph_data_array['graph_start'])) . ' To ' . str_replace(':', '\:', date($graph_date, $graph_data_array['graph_end'])) . '\\c"' . RRD_NL . 'COMMENT:" \\n"' . RRD_NL;
|
|
}
|
|
}
|
|
|
|
if (CACTI_WEB) {
|
|
cacti_system_zone_set();
|
|
}
|
|
|
|
return $graph_legend;
|
|
}
|
|
|
|
function rrdtool_function_theme_font_options(array &$graph_data_array) : string {
|
|
global $rrdcolors, $rrdfonts, $rrdborder;
|
|
|
|
// implement theme colors
|
|
$graph_opts = '';
|
|
$themefonts = [];
|
|
$themecolors = 'rrdcolors';
|
|
$themeborder = 'rrdborder';
|
|
|
|
if (isset($graph_data_array['graph_theme'])) {
|
|
$theme = cacti_validate_theme((string) $graph_data_array['graph_theme']);
|
|
} else {
|
|
$theme = cacti_validate_theme(get_selected_theme());
|
|
}
|
|
|
|
$rrdtheme = CACTI_PATH_INCLUDE . '/themes/' . $theme . '/rrdtheme.php';
|
|
|
|
if (file_exists($rrdtheme) && is_readable($rrdtheme)) {
|
|
$rrdversion = get_rrdtool_version();
|
|
include($rrdtheme);
|
|
|
|
if (isset($_COOKIE['CactiColorMode']) && in_array($_COOKIE['CactiColorMode'], ['dark', 'light', 'dark-dimmed'], true)) {
|
|
$themecolors = 'rrdcolors_' . $_COOKIE['CactiColorMode'];
|
|
$themeborder = 'rrdborder_' . $_COOKIE['CactiColorMode'];
|
|
|
|
if (!isset(${$themecolors}) || !is_array(${$themecolors})) { // @phpstan-ignore-line
|
|
$themecolors = 'rrdcolors';
|
|
$themeborder = 'rrdborder';
|
|
}
|
|
}
|
|
|
|
if (isset(${$themecolors}) && is_array(${$themecolors})) { // @phpstan-ignore-line
|
|
foreach (${$themecolors} as $colortag => $color) { // @phpstan-ignore-line
|
|
$graph_opts .= '--color ' . cacti_strtoupper($colortag) . '#' . cacti_strtoupper($color) . RRD_NL;
|
|
}
|
|
}
|
|
|
|
if (isset(${$themeborder}) && cacti_version_compare($rrdversion, '1.4', '>=')) { // @phpstan-ignore-line
|
|
$graph_opts .= '--border ' . ${$themeborder} . RRD_NL; // @phpstan-ignore-line
|
|
}
|
|
|
|
if (isset($rrdfonts)) {
|
|
$themefonts = $rrdfonts;
|
|
}
|
|
}
|
|
|
|
// title fonts
|
|
$graph_opts .= rrdtool_function_set_font('title', ((!empty($graph_data_array['graph_nolegend'])) ? $graph_data_array['graph_nolegend'] : ''), $themefonts);
|
|
|
|
// axis fonts
|
|
$graph_opts .= rrdtool_function_set_font('axis', '', $themefonts);
|
|
|
|
// legend fonts
|
|
$graph_opts .= rrdtool_function_set_font('legend', '', $themefonts);
|
|
|
|
// unit fonts
|
|
$graph_opts .= rrdtool_function_set_font('unit', '', $themefonts);
|
|
|
|
// watermark fonts
|
|
if (isset($rrdversion) && cacti_version_compare($rrdversion, '1.3', '>')) {
|
|
$graph_opts .= rrdtool_function_set_font('watermark', '', $themefonts);
|
|
}
|
|
|
|
return $graph_opts;
|
|
}
|
|
|
|
function rrdtool_set_font(string $type, string $no_legend = '', array $themefonts = []) : string {
|
|
return rrdtool_function_set_font($type, $no_legend, $themefonts);
|
|
}
|
|
|
|
function rrdtool_function_set_font(string $type, string $no_legend, array $themefonts) : string {
|
|
if (read_config_option('font_method') == 0) {
|
|
if (read_user_setting('custom_fonts') == 'on') {
|
|
$font = read_user_setting($type . '_font');
|
|
$size = read_user_setting($type . '_size');
|
|
} else {
|
|
$font = read_config_option($type . '_font');
|
|
$size = read_config_option($type . '_size');
|
|
}
|
|
} elseif (isset($themefonts[$type]['font']) && isset($themefonts[$type]['size'])) {
|
|
$font = $themefonts[$type]['font'];
|
|
$size = $themefonts[$type]['size'];
|
|
} else {
|
|
return '';
|
|
}
|
|
|
|
if ($font != '') {
|
|
/* verifying all possible pango font params is too complex to be tested here
|
|
* so we only escape the font
|
|
*/
|
|
$font = cacti_escapeshellarg($font);
|
|
}
|
|
|
|
if ($type == 'title') {
|
|
if (!empty($no_legend)) {
|
|
$size *= .70;
|
|
} elseif (($size <= 4) || !is_numeric($size)) {
|
|
$size = 12;
|
|
}
|
|
} elseif (($size <= 4) || !is_numeric($size)) {
|
|
$size = 8;
|
|
}
|
|
|
|
return '--font ' . cacti_strtoupper($type) . ':' . floatval($size) . ':' . $font . RRD_NL;
|
|
}
|
|
|
|
function rrd_substitute_host_query_data(string $txt_graph_item, array $graph, array $graph_item) : string {
|
|
// replace host variables in graph elements
|
|
$host_id = 0;
|
|
|
|
if (!preg_match('/(\|query_|\|host_|\|input_|\|poller_|\|site_|\|stream_)/', $txt_graph_item)) {
|
|
return $txt_graph_item;
|
|
}
|
|
|
|
if (empty($graph['host_id'])) {
|
|
// if graph has no associated host determine host_id from graph item data source
|
|
if (!empty($graph_item['data_source_host_id'])) {
|
|
$host_id = $graph_item['data_source_host_id'];
|
|
} elseif (isset($graph_item['local_data_id']) && !empty($graph_item['local_data_id'])) {
|
|
$host_id = db_fetch_cell_prepared('SELECT host_id
|
|
FROM data_local
|
|
WHERE id = ?',
|
|
[$graph_item['local_data_id']]);
|
|
}
|
|
} else {
|
|
$host_id = $graph['host_id'];
|
|
}
|
|
|
|
$txt_graph_item = substitute_host_data($txt_graph_item, '|', '|', $host_id);
|
|
|
|
// replace query variables in graph elements
|
|
if (str_contains($txt_graph_item, '|query_')) {
|
|
if (isset($graph_item['snmp_query_id'])) {
|
|
$txt_graph_item = substitute_snmp_query_data($txt_graph_item, $host_id, $graph_item['snmp_query_id'], $graph_item['snmp_index']);
|
|
} elseif (isset($graph['snmp_query_id'])) {
|
|
$txt_graph_item = substitute_snmp_query_data($txt_graph_item, $host_id, $graph['snmp_query_id'], $graph['snmp_index']);
|
|
}
|
|
}
|
|
|
|
// replace query variables in graph elements
|
|
if (str_contains($txt_graph_item, '|input_') && isset($graph_item['local_data_id'])) {
|
|
return substitute_data_input_data($txt_graph_item, $graph, $graph_item['local_data_id']);
|
|
}
|
|
|
|
if (str_contains($txt_graph_item, '|poller_') && isset($graph_item['local_data_id'])) {
|
|
return substitute_poller_data($txt_graph_item, $graph, $graph_item['local_data_id']);
|
|
}
|
|
|
|
if (str_contains($txt_graph_item, '|site_') && isset($graph_item['local_data_id'])) {
|
|
return substitute_site_data($txt_graph_item, $graph, $graph_item['local_data_id']);
|
|
}
|
|
|
|
if (str_contains($txt_graph_item, '|stream_') && isset($graph_item['local_data_id'])) {
|
|
if (function_exists('stream_substitute_query_data')) {
|
|
return stream_substitute_query_data($txt_graph_item, $graph, $graph_item['local_data_id']);
|
|
}
|
|
} else {
|
|
return $txt_graph_item;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function rrdtool_function_get_resstep(mixed $local_data_ids, int $graph_start, int $graph_end, string $type = 'res') : int {
|
|
if (!is_array($local_data_ids)) {
|
|
$local_data_ids = [$local_data_ids];
|
|
}
|
|
|
|
$local_data_ids = array_values(array_unique($local_data_ids, SORT_REGULAR));
|
|
|
|
$time = time();
|
|
|
|
if ($graph_start < 0) {
|
|
$graph_start = $time + $graph_start;
|
|
}
|
|
|
|
if ($graph_end < 0) {
|
|
$graph_end = $time + $graph_end;
|
|
}
|
|
|
|
if (!cacti_sizeof($local_data_ids)) {
|
|
return 0;
|
|
}
|
|
|
|
$placeholders = implode(', ', array_fill(0, cacti_sizeof($local_data_ids), '?'));
|
|
$resolutions = db_fetch_assoc_prepared("SELECT dtd.local_data_id, dsp.step, dspr.steps, dspr.rows, dspr.timespan
|
|
FROM data_source_profiles AS dsp
|
|
INNER JOIN data_source_profiles_rra AS dspr
|
|
ON dspr.data_source_profile_id = dsp.id
|
|
INNER JOIN data_template_data AS dtd
|
|
ON dtd.data_source_profile_id = dsp.id
|
|
WHERE dtd.local_data_id IN ($placeholders)
|
|
ORDER BY dtd.local_data_id, dsp.step, dspr.steps ASC",
|
|
$local_data_ids);
|
|
|
|
$resolutions_by_data_source = [];
|
|
|
|
foreach ($resolutions as $resolution) {
|
|
$resolutions_by_data_source[$resolution['local_data_id']][] = $resolution;
|
|
}
|
|
|
|
foreach ($local_data_ids as $local_data_id) {
|
|
foreach ($resolutions_by_data_source[$local_data_id] ?? [] as $resolution) {
|
|
if ($graph_start > ($time - ($resolution['step'] * $resolution['steps'] * $resolution['rows']))) {
|
|
return $type == 'res' ?
|
|
$resolution['step'] * $resolution['steps'] :
|
|
$resolution['step'];
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* rrdtool_file_exists - given a data source path check either
|
|
* the local file system of the rrdtool proxy to see if the
|
|
* data source path exists.
|
|
*
|
|
* @param string $data_source_path The data source rrdfile path
|
|
* @param mixed $rrdtool_pipe The rrdtool pipe if available
|
|
*
|
|
* @return bool A boolean to tell if the file exists
|
|
*/
|
|
function rrdtool_file_exists(string $data_source_path, mixed $rrdtool_pipe = null) : bool {
|
|
if (read_config_option('storage_location')) {
|
|
if (!rrdtool_execute('file_exists ' . cacti_escapeshellarg($data_source_path), true, RRDTOOL_OUTPUT_BOOLEAN, $rrdtool_pipe, 'POLLER')) {
|
|
return false;
|
|
}
|
|
} elseif (!file_exists($data_source_path)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* rrdtool_function_info - given a data source id, return rrdtool info array
|
|
*
|
|
* @param int $local_data_id Data source id
|
|
* @param mixed $rrdtool_pipe The rrdtool pipe if available
|
|
*
|
|
* @return mixed An array containing all data from rrdtool info command
|
|
*/
|
|
function rrdtool_function_info(int $local_data_id, mixed $rrdtool_pipe = null) : mixed {
|
|
// Get the path to rrdtool file
|
|
$data_source_path = get_data_source_path($local_data_id, true);
|
|
|
|
if (!rrdtool_file_exists($data_source_path, $rrdtool_pipe)) {
|
|
return false;
|
|
}
|
|
|
|
// Execute rrdtool info command
|
|
$cmd_line = ' info ' . cacti_escapeshellarg($data_source_path);
|
|
$output = rrdtool_execute($cmd_line, RRDTOOL_OUTPUT_NULL, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe);
|
|
|
|
if (!is_string($output) || $output == '') {
|
|
return false;
|
|
}
|
|
|
|
// Hack for i18n
|
|
if (str_contains($output, ',')) {
|
|
$output = str_replace(',', '.', $output);
|
|
}
|
|
|
|
// Parse the output
|
|
$matches = [];
|
|
/** @var array<string, mixed> $rrd_info */
|
|
$rrd_info = ['rra' => [], 'ds' => []];
|
|
$output = explode("\n", $output);
|
|
|
|
foreach ($output as $line) {
|
|
$line = trim($line);
|
|
|
|
if (preg_match('/^ds\[(\S+)\]\.(\S+) = (\S+)$/', $line, $matches)) {
|
|
$rrd_info['ds'][$matches[1]][$matches[2]] = trim($matches[3], '"');
|
|
} elseif (preg_match('/^rra\[(\S+)\]\.(\S+)\[(\S+)\]\.(\S+) = (\S+)$/', $line, $matches)) {
|
|
$rrd_info['rra'][$matches[1]][$matches[2]][$matches[3]][$matches[4]] = trim($matches[5], '"');
|
|
} elseif (preg_match('/^rra\[(\S+)\]\.(\S+) = (\S+)$/', $line, $matches)) {
|
|
$rrd_info['rra'][$matches[1]][$matches[2]] = trim($matches[3], '"');
|
|
} elseif (preg_match("/^(\S+) = \"(\S+)\"$/", $line, $matches)) {
|
|
$rrd_info[$matches[1]] = trim($matches[2], '"');
|
|
} elseif (preg_match('/^(\S+) = (\S+)$/', $line, $matches)) {
|
|
$rrd_info[$matches[1]] = trim($matches[2], '"');
|
|
}
|
|
}
|
|
|
|
$output = '';
|
|
$matches = [];
|
|
|
|
// Return parsed values
|
|
return $rrd_info;
|
|
}
|
|
|
|
function rrdtool_function_info_from_ds(int $data_source_id) : array {
|
|
global $data_source_types, $consolidation_functions;
|
|
|
|
// construct the skeleton of the info array
|
|
$info_array['rra'] = [];
|
|
$info_array['ds'] = [];
|
|
$info_array['step'] = 0;
|
|
$info_array['filename'] = clean_up_path(get_data_source_path($data_source_id, true));
|
|
|
|
// get cacti header information for given data source id
|
|
$cacti_header_array = db_fetch_row_prepared('SELECT
|
|
local_data_template_data_id, rrd_step, data_source_profile_id
|
|
FROM data_template_data
|
|
WHERE local_data_id = ?',
|
|
[$data_source_id]);
|
|
|
|
$data_template_id = db_fetch_cell_prepared('SELECT data_template_id
|
|
FROM data_local
|
|
WHERE id = ?',
|
|
[$data_source_id]);
|
|
|
|
// get cacti DS information
|
|
if ($data_template_id > 0) {
|
|
$cacti_ds_array = db_fetch_assoc_prepared('SELECT DISTINCT dtr.id, dtr.data_source_name, dtr.data_source_type_id,
|
|
dtr.rrd_heartbeat, dtr.rrd_maximum, dtr.rrd_minimum
|
|
FROM data_template_rrd AS dtr
|
|
INNER JOIN graph_templates_item AS gti
|
|
ON dtr.id = gti.task_item_id
|
|
WHERE local_data_id = ?',
|
|
[$data_source_id]);
|
|
} else {
|
|
$cacti_ds_array = db_fetch_assoc_prepared('SELECT DISTINCT dtr.id, dtr.data_source_name, dtr.data_source_type_id,
|
|
dtr.rrd_heartbeat, dtr.rrd_maximum, dtr.rrd_minimum
|
|
FROM data_template_rrd AS dtr
|
|
WHERE local_data_id = ?',
|
|
[$data_source_id]);
|
|
}
|
|
|
|
if (cacti_sizeof($cacti_header_array) && cacti_sizeof($cacti_ds_array)) {
|
|
$info_array['step'] = $cacti_header_array['rrd_step'];
|
|
|
|
// get cacti RRA information
|
|
$cacti_rra_array = db_fetch_assoc_prepared('SELECT
|
|
dspc.consolidation_function_id AS cf,
|
|
dsp.x_files_factor AS xff,
|
|
dsp.heartbeat,
|
|
dspr.steps AS steps,
|
|
dspr.rows AS `rows`
|
|
FROM data_source_profiles AS dsp
|
|
INNER JOIN data_source_profiles_cf AS dspc
|
|
ON dsp.id = dspc.data_source_profile_id
|
|
INNER JOIN data_source_profiles_rra AS dspr
|
|
ON dsp.id = dspr.data_source_profile_id
|
|
WHERE dsp.id = ?
|
|
ORDER BY dspc.consolidation_function_id, dspr.steps',
|
|
[$cacti_header_array['data_source_profile_id']]);
|
|
|
|
// get the data source information
|
|
$data_local = db_fetch_row_prepared('SELECT host_id, snmp_query_id, snmp_index
|
|
FROM data_local
|
|
WHERE id = ?',
|
|
[$data_source_id]);
|
|
|
|
// get the speed just in case we have replacement data
|
|
$speed = is_array($data_local) ? rrdtool_function_interface_speed($data_local) : '0';
|
|
|
|
// complete the 'ds' component of the info array
|
|
foreach ($cacti_ds_array as $index => $ds) {
|
|
$info_array['ds'][$ds['data_source_name']]['index'] = $index;
|
|
$info_array['ds'][$ds['data_source_name']]['type'] = $data_source_types[$ds['data_source_type_id']];
|
|
$info_array['ds'][$ds['data_source_name']]['min'] = $ds['rrd_minimum'];
|
|
|
|
if ($ds['rrd_maximum'] == '|query_ifSpeed|' || $ds['rrd_maximum'] == '|query_ifHighSpeed|') {
|
|
$info_array['ds'][$ds['data_source_name']]['max'] = $speed;
|
|
} else {
|
|
$info_array['ds'][$ds['data_source_name']]['max'] = $ds['rrd_maximum'];
|
|
}
|
|
|
|
$info_array['ds'][$ds['data_source_name']]['minimal_heartbeat'] = $ds['rrd_heartbeat'];
|
|
}
|
|
|
|
// complete the 'rra' component of the info array
|
|
if (cacti_sizeof($cacti_rra_array)) {
|
|
foreach ($cacti_rra_array as $index => $rra) {
|
|
foreach ($cacti_ds_array as $ds) {
|
|
$info_array['rra'][$index]['cf'] = $consolidation_functions[$rra['cf']];
|
|
$info_array['rra'][$index]['rows'] = $rra['rows'];
|
|
$info_array['rra'][$index]['xff'] = $rra['xff'];
|
|
$info_array['rra'][$index]['steps'] = $rra['steps'];
|
|
}
|
|
}
|
|
|
|
return $info_array;
|
|
} else {
|
|
return [];
|
|
}
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* rrdtool_function_contains_cf verifies if the RRDfile contains the 'MAX' consolidation function
|
|
*
|
|
* @param int $local_data_id The id of the data source
|
|
* @param string $cf The consolidation function to search for
|
|
* @param mixed $rrdtool_pipe The rrdtool pipe if available
|
|
*
|
|
* @return bool true or false depending on the result
|
|
*/
|
|
function rrdtool_function_contains_cf(int $local_data_id, string $cf, mixed $rrdtool_pipe = null) : bool {
|
|
$info = rrdtool_function_info($local_data_id, $rrdtool_pipe);
|
|
|
|
if (cacti_sizeof($info)) {
|
|
if (isset($info['rra'])) {
|
|
foreach ($info['rra'] as $ds) {
|
|
if ($ds['cf'] == $cf) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* rrdtool_cacti_compare - compares cacti information to rrd file information
|
|
*
|
|
* @param int $data_source_id The id of the data source
|
|
* @param array $info The rrdtool info as an array
|
|
*
|
|
* @return array Array build like $info defining html class in case of error
|
|
*/
|
|
function rrdtool_cacti_compare(int $data_source_id, array &$info) : array {
|
|
global $data_source_types, $consolidation_functions;
|
|
|
|
/**
|
|
* This function will be passed the RRDtool info rrdfile output
|
|
* formatted as an array with multiple indexes. They include:
|
|
*
|
|
* 'ds' => An array of data source items including name, min/max, heartbeat, and type
|
|
* 'rra' => An array of consolidation functions, rows, xff and other misc data from
|
|
* the RRDfile.
|
|
*
|
|
* The the $cacti_info array includes the same structure, but from the current database
|
|
* settings, which may differ from the original rrdfile that is on disk.
|
|
*
|
|
* Once we have these two pieces of information, we can do a comparison and provide some
|
|
* guidance as to how to correct the format of the RRDfile. Some of the possible
|
|
* differences can result in what would effectively be missing data, so care must be taken
|
|
* when providing these recommendations.
|
|
*/
|
|
|
|
$cacti_info = rrdtool_function_info_from_ds($data_source_id);
|
|
|
|
// create an empty diff array to hold the differences
|
|
$diff = [];
|
|
|
|
// initialize some additional variables
|
|
$cacti_rra_id = -1;
|
|
$cacti_rra = -1;
|
|
|
|
if ($cacti_info['step'] != $info['step']) {
|
|
$diff['step'] = __("The required RRDfile step size is '%s' but observed step is '%s'", $cacti_info['step'], $info['step']);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------------------
|
|
* data source information
|
|
-----------------------------------------------------------------------------------*/
|
|
if (cacti_sizeof($cacti_info) && cacti_sizeof($info)) {
|
|
// get the heartbeat from the profile
|
|
$profile_heartbeat = db_fetch_cell_prepared('SELECT heartbeat
|
|
FROM data_source_profiles AS dsp
|
|
INNER JOIN data_template_data AS dtd
|
|
ON dsp.id = dtd.data_source_profile_id
|
|
WHERE dtd.local_data_id = ?',
|
|
[$data_source_id]);
|
|
|
|
// cursory checks on total data sources
|
|
if (isset($cacti_info['ds']) && isset($info['ds'])) {
|
|
if (cacti_sizeof($cacti_info['ds']) != cacti_sizeof($info['ds'])) {
|
|
// Need to warn here about the total number of data sources not matchhing
|
|
}
|
|
|
|
foreach ($cacti_info['ds'] as $data_source_name => $data_source) {
|
|
/**
|
|
* This should be a rare case today where the RRDfile does not
|
|
* have the data source, but Cacti does.
|
|
*/
|
|
if (!isset($info['ds'][$data_source_name])) {
|
|
// cacti knows this ds, but the rrd file does not
|
|
$info['ds'][$data_source_name]['type'] = $data_source['type'];
|
|
$info['ds'][$data_source_name]['minimal_heartbeat'] = $data_source['minimal_heartbeat'];
|
|
$info['ds'][$data_source_name]['min'] = $data_source['min'];
|
|
$info['ds'][$data_source_name]['max'] = $data_source['max'];
|
|
$info['ds'][$data_source_name]['seen'] = true;
|
|
|
|
continue;
|
|
}
|
|
|
|
/**
|
|
* Accommodate a Cacti bug where the heartbeat was not
|
|
* propagated.
|
|
*/
|
|
if ($data_source['minimal_heartbeat'] != $profile_heartbeat) {
|
|
cacti_log(sprintf('NOTE: Incorrect Data Source heartbeat found and corrected for Local Data ID %s and Data Source \'%s\'', $data_source_id, $data_source_name), false, 'DSDEBUG');
|
|
|
|
db_execute_prepared('UPDATE data_template_rrd
|
|
SET rrd_heartbeat = ?
|
|
WHERE local_data_id = ?
|
|
AND data_source_name = ?',
|
|
[$profile_heartbeat, $data_source_id, $data_source_name]);
|
|
|
|
$cacti_info['ds'][$data_source_name]['minimal_heartbeat'] = $profile_heartbeat;
|
|
}
|
|
|
|
// verity that all data sources are seen
|
|
if (isset($info['ds'][$data_source_name])) {
|
|
$info['ds'][$data_source_name]['seen'] = true;
|
|
}
|
|
|
|
// check the data source type
|
|
if ($data_source['type'] != $info['ds'][$data_source_name]['type']) {
|
|
$diff['ds'][$data_source_name]['type'] = __("Type for Data Source '%s' should be '%s'", $data_source_name, $data_source['type']);
|
|
$diff['tune'][] = cacti_escapeshellarg($info['filename']) . ' --data-source-type ' . cacti_escapeshellarg($data_source_name . ':' . $data_source['type']);
|
|
}
|
|
|
|
// check the minimal heartbeat
|
|
if ($data_source['minimal_heartbeat'] != $info['ds'][$data_source_name]['minimal_heartbeat']) {
|
|
$diff['ds'][$data_source_name]['minimal_heartbeat'] = __("Heartbeat for Data Source '%s' should be '%s'", $data_source_name, $data_source['minimal_heartbeat']);
|
|
$diff['tune'][] = cacti_escapeshellarg($info['filename']) . ' --heartbeat ' . cacti_escapeshellarg($data_source_name . ':' . $data_source['minimal_heartbeat']);
|
|
}
|
|
|
|
// check the minimums and maximums and correct for legacy behavior
|
|
if ($data_source['min'] != $info['ds'][$data_source_name]['min']) {
|
|
if (($data_source['min'] == '0' || $data_source['max'] == 'U') && $info['ds'][$data_source_name]['min'] == 'NaN') {
|
|
$info['ds'][$data_source_name]['min'] = 'U';
|
|
}
|
|
}
|
|
|
|
if ($data_source['min'] != $info['ds'][$data_source_name]['min']) {
|
|
$diff['ds'][$data_source_name]['min'] = __("RRD minimum for Data Source '%s' should be '%s'", $data_source_name, $data_source['min']);
|
|
$diff['tune'][] = cacti_escapeshellarg($info['filename']) . ' --minimum ' . cacti_escapeshellarg($data_source_name . ':' . $data_source['min']);
|
|
}
|
|
}
|
|
}
|
|
|
|
// if there are data sources missing log that now
|
|
foreach ($info['ds'] as $data_source_name => $data_source) {
|
|
if (!isset($data_source['seen'])) {
|
|
$diff['ds'][$data_source_name]['error'] = __("DS '%s' missing in Cacti definition", $data_source_name);
|
|
}
|
|
}
|
|
|
|
if (isset($cacti_info['rra']) && isset($info['rra'])) {
|
|
$resize = true; // assume a resize operation as long as no rra duplicates are found
|
|
|
|
// scan cacti rra information for duplicates of (CF, STEPS)
|
|
foreach ($cacti_info['rra'] as $index => $rra) {
|
|
$cf = $rra['cf'];
|
|
$steps = $rra['steps'];
|
|
|
|
foreach ($cacti_info['rra'] as $cacti_rra_id => $cacti_rra) {
|
|
if ($cf == $cacti_rra['cf'] && $steps == $cacti_rra['steps'] && ($index != $cacti_rra_id)) {
|
|
$diff['rra'][$index]['error'] = __("Cacti RRA '%s' has same CF/steps (%s, %s) as '%s'", $index, $cf, $steps, $cacti_rra_id);
|
|
$diff['rra'][$cacti_rra_id]['error'] = __("Cacti RRA '%s' has same CF/steps (%s, %s) as '%s'", $cacti_rra_id, $cf, $steps, $index);
|
|
$resize = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// scan file rra information for duplicates of (CF, PDP_PER_ROWS)
|
|
foreach ($info['rra'] as $index => $rra) {
|
|
$cf = $rra['cf'];
|
|
$steps = $rra['pdp_per_row'];
|
|
$i = 0;
|
|
|
|
foreach ($info['rra'] as $file_rra_id => $file_rra) {
|
|
if (($cf == $file_rra['cf']) && ($steps == $file_rra['pdp_per_row']) && ($index != $file_rra_id)) {
|
|
$diff['rra'][$i]['error'] = __("File RRA '%s' has same CF/steps (%s, %s) as '%s'", $index, $cf, $steps, $file_rra_id);
|
|
$diff['rra'][$file_rra_id]['error'] = __("File RRA '%s' has same CF/steps (%s, %s) as '%s'", $file_rra_id, $cf, $steps, $index);
|
|
|
|
$resize = false;
|
|
}
|
|
|
|
$i++;
|
|
}
|
|
}
|
|
|
|
foreach ($cacti_info['rra'] as $cacti_rra_id => $cacti_rra) {
|
|
/**
|
|
* find matching rra info from rrd file
|
|
* do NOT assume, that rra sequence is kept ($cacti_rra_id != $file_rra_id may happen)!
|
|
* Match is assumed, if CF and STEPS/PDP_PER_ROW match; so go for it
|
|
*/
|
|
foreach ($info['rra'] as $file_rra_id => $file_rra) {
|
|
// in case of mismatch, $file_rra['pdp_per_row'] might not be defined
|
|
if (!isset($file_rra['pdp_per_row'])) {
|
|
$file_rra['pdp_per_row'] = 0;
|
|
}
|
|
|
|
// correct issue with older rrdtools
|
|
$file_rra['cf'] = trim($file_rra['cf'], '"');
|
|
|
|
if ($cacti_rra['cf'] == $file_rra['cf'] && $cacti_rra_id == $file_rra_id) {
|
|
if (isset($info['rra'][$file_rra_id]['seen'])) {
|
|
continue;
|
|
}
|
|
|
|
// mark both rra id's as seen to avoid printing them as non-matching
|
|
$info['rra'][$file_rra_id]['seen'] = true;
|
|
$cacti_info['rra'][$cacti_rra_id]['seen'] = true;
|
|
|
|
if ($cacti_rra['steps'] != $file_rra['pdp_per_row']) {
|
|
$diff['rra'][$file_rra_id]['pdp_per_row_error'] = __("The pdp_per_row of '%s' is invalid for RRA '%s' should be '%s'. Consider deleting and allowing Cacti to re-create RRDfile.", $file_rra['pdp_per_row'], $cacti_rra_id, $cacti_rra['steps']);
|
|
}
|
|
|
|
if ($cacti_rra['xff'] != $file_rra['xff']) {
|
|
$diff['rra'][$file_rra_id]['xff'] = __("The XFF for Cacti RRA id is '%s' but should be '%s'", $cacti_rra_id, $cacti_rra['xff']);
|
|
}
|
|
|
|
if ($cacti_rra['rows'] != $file_rra['rows'] && $resize) {
|
|
$diff['rra'][$file_rra_id]['rows'] = __("The number of rows for Cacti RRA id '%s' is incorrect. The number of rows are '%s' but should be '%s'", $cacti_rra_id, $file_rra['rows'], $cacti_rra['rows']);
|
|
|
|
if ($cacti_rra['rows'] > $file_rra['rows']) {
|
|
$diff['resize'][] = cacti_escapeshellarg($info['filename']) . ' ' . $cacti_rra_id . ' GROW ' . ($cacti_rra['rows'] - $file_rra['rows']);
|
|
} else {
|
|
$diff['resize'][] = cacti_escapeshellarg($info['filename']) . ' ' . $cacti_rra_id . ' SHRINK ' . ($file_rra['rows'] - $cacti_rra['rows']);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// if cacti knows an rra that has no match, consider this as an error
|
|
if ($cacti_rra_id >= 0 && $cacti_rra >= 0 && !isset($cacti_info['rra'][$cacti_rra_id]['seen'])) {
|
|
// add to info array for printing, the index $cacti_rra_id has no real meaning
|
|
$info['rra']['cacti_' . $cacti_rra_id]['cf'] = $cacti_rra['cf'];
|
|
$info['rra']['cacti_' . $cacti_rra_id]['steps'] = $cacti_rra['steps'];
|
|
$info['rra']['cacti_' . $cacti_rra_id]['xff'] = $cacti_rra['xff'];
|
|
$info['rra']['cacti_' . $cacti_rra_id]['rows'] = $cacti_rra['rows'];
|
|
$diff['rra']['cacti_' . $cacti_rra_id]['error'] = __("The RRA '%s' missing in the existing Cacti RRDfile", $cacti_rra_id);
|
|
}
|
|
}
|
|
|
|
// if the rrd file has an rra that has no cacti match, consider this as an error
|
|
foreach ($info['rra'] as $file_rra_id => $file_rra) {
|
|
if (!isset($info['rra'][$file_rra_id]['seen'])) {
|
|
$diff['rra'][$file_rra_id]['error'] = __("RRA '%s' missing in Cacti definition", $file_rra_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $diff;
|
|
}
|
|
|
|
/**
|
|
* rrdtool_info2html - take output from rrdtool info array and build html table
|
|
*
|
|
* @param array $info_array An array of rrdtool info data
|
|
* @param array $diff An array of differences between definition and current rrd file settings
|
|
*
|
|
* @return void
|
|
*/
|
|
function rrdtool_info2html(array $info_array, array $diff = []) : void {
|
|
include_once(CACTI_PATH_LIBRARY . '/time.php');
|
|
|
|
html_start_box(__('RRD File Information'), '100%', false, 3, 'center', '');
|
|
|
|
// header data
|
|
$header_items = [
|
|
['display' =>__('Header'), 'align' => 'left'],
|
|
['display' => '', 'align' => 'left']
|
|
];
|
|
|
|
html_header($header_items, 1);
|
|
|
|
// add human readable timestamp
|
|
if (isset($info_array['last_update'])) {
|
|
$info_array['last_update'] .= ' [' . date(CACTI_DATE_TIME_FORMAT, $info_array['last_update']) . ']';
|
|
}
|
|
|
|
$loop = [
|
|
'filename' => $info_array['filename'],
|
|
'rrd_version' => $info_array['rrd_version'],
|
|
'step' => $info_array['step'],
|
|
'last_update' => $info_array['last_update']];
|
|
|
|
foreach ($loop as $key => $value) {
|
|
form_alternate_row($key, true);
|
|
form_selectable_cell($key, 'key');
|
|
form_selectable_cell($value, 'value', '', ((isset($diff[$key]) ? 'color:red' : '')));
|
|
form_end_row();
|
|
}
|
|
|
|
html_end_box();
|
|
|
|
// data sources
|
|
$header_items = [
|
|
['display' => __('Data Source Items'), 'align' => 'left'],
|
|
['display' => __('Type'), 'align' => 'left'],
|
|
['display' => __('Minimal Heartbeat'), 'align' => 'right'],
|
|
['display' => __('Min'), 'align' => 'right'],
|
|
['display' => __('Max'), 'align' => 'right'],
|
|
['display' => __('Last DS'), 'align' => 'right'],
|
|
['display' => __('Value'), 'align' => 'right'],
|
|
['display' => __('Unknown Sec'), 'align' => 'right']
|
|
];
|
|
|
|
html_start_box('', '100%', false, 3, 'center', '');
|
|
|
|
html_header($header_items, 1);
|
|
|
|
if (cacti_sizeof($info_array['ds'])) {
|
|
foreach ($info_array['ds'] as $key => $value) {
|
|
form_alternate_row('line' . $key, true);
|
|
|
|
form_selectable_cell($key, 'name', '', (isset($diff['ds'][$key]['error']) ? 'color:red' : ''));
|
|
form_selectable_cell(($value['type'] ?? ''), 'type', '', (isset($diff['ds'][$key]['type']) ? 'color:red' : ''));
|
|
form_selectable_cell(($value['minimal_heartbeat'] ?? ''), 'minimal_heartbeat', '', (isset($diff['ds'][$key]['minimal_heartbeat']) ? 'color:red, text-align:right' : 'text-align:right'));
|
|
|
|
if (isset($value['min'])) {
|
|
if ($value['min'] == 'U') {
|
|
form_selectable_cell($value['min'], 'min', '', 'right');
|
|
} elseif (is_numeric($value['min'])) {
|
|
form_selectable_cell(number_format_i18n($value['min']), 'min', '', 'right');
|
|
} else {
|
|
form_selectable_cell($value['min'], 'min', '', 'color:red;text-align:right');
|
|
}
|
|
} else {
|
|
form_selectable_cell(__('Unknown'), 'min', '', 'color:red;text-align:right');
|
|
}
|
|
|
|
if (isset($value['max'])) {
|
|
if ($value['max'] == 'U' || $value['max'] == 'NaN') {
|
|
form_selectable_cell($value['max'], 'max', '', 'right');
|
|
} elseif (is_numeric($value['max'])) {
|
|
form_selectable_cell(number_format_i18n($value['max']), 'max', '', 'right');
|
|
} else {
|
|
form_selectable_cell($value['max'], 'max', '', 'color:red;text-align:right');
|
|
}
|
|
} else {
|
|
form_selectable_cell(__('Unknown'), 'max', '', 'color:red;text-align:right');
|
|
}
|
|
|
|
form_selectable_cell(((isset($value['last_ds']) && is_numeric($value['last_ds']) ? number_format_i18n($value['last_ds']) : ($value['last_ds'] ?? ''))), 'last_ds', '', 'text-align:right');
|
|
form_selectable_cell(((isset($value['value']) ? (is_numeric($value['value']) ? number_format_i18n($value['value']) : $value['value']) : '')), 'value', '', 'text-align:right');
|
|
form_selectable_cell(((isset($value['unknown_sec']) && is_numeric($value['unknown_sec']) ? number_format_i18n($value['unknown_sec']) : ($value['unknown_sec'] ?? ''))), 'unknown_sec', '', 'text-align:right');
|
|
|
|
form_end_row();
|
|
}
|
|
}
|
|
|
|
html_end_box();
|
|
|
|
// round robin archive
|
|
$header_items = [
|
|
['display' => __('Round Robin Archive'), 'align' => 'left'],
|
|
['display' => __('Consolidation Function'), 'align' => 'left'],
|
|
['display' => __('Rows'), 'align' => 'right'],
|
|
['display' => __('Cur Row'), 'align' => 'right'],
|
|
['display' => __('PDP per Row'), 'align' => 'right'],
|
|
['display' => __('X-Files Factor'), 'align' => 'right'],
|
|
['display' => __('CDP Prep Value (0)'), 'align' => 'right'],
|
|
['display' => __('CDP Unknown Data points (0)'), 'align' => 'right']
|
|
];
|
|
|
|
html_start_box('', '100%', false, 3, 'center', '');
|
|
|
|
html_header($header_items, 1);
|
|
|
|
if (cacti_sizeof($info_array['rra'])) {
|
|
foreach ($info_array['rra'] as $key => $value) {
|
|
form_alternate_row('line_' . $key, true);
|
|
|
|
form_selectable_cell($key, 'name', '', (isset($diff['rra'][$key]['error']) ? 'color:red' : ''));
|
|
form_selectable_cell(($value['cf'] ?? ''), 'cf');
|
|
form_selectable_cell(($value['rows'] ?? ''), 'rows', '', (isset($diff['rra'][$key]['rows']) ? 'color:red;text-align:right' : 'text-align:right'));
|
|
form_selectable_cell(($value['cur_row'] ?? ''), 'cur_row', '', 'text-align:right');
|
|
form_selectable_cell(($value['pdp_per_row'] ?? ''), 'pdp_per_row', '', 'text-align:right');
|
|
form_selectable_cell((isset($value['xff']) ? floatval($value['xff']) : ''), 'xff', '', (isset($diff['rra'][$key]['xff']) ? 'color:red;text-align:right' : 'text-align:right'));
|
|
form_selectable_cell((isset($value['cdp_prep'][0]['value']) ? ((cacti_strtolower($value['cdp_prep'][0]['value']) == 'nan') ? $value['cdp_prep'][0]['value'] : floatval($value['cdp_prep'][0]['value'])) : ''), 'value', '', 'text-align:right');
|
|
form_selectable_cell(($value['cdp_prep'][0]['unknown_datapoints'] ?? ''), 'unknown_datapoints', '', 'text-align:right');
|
|
|
|
form_end_row();
|
|
}
|
|
}
|
|
|
|
html_end_box();
|
|
}
|
|
|
|
/**
|
|
* print_leaves - A function used by the rrdtool_tune function to output data
|
|
*
|
|
* @param array $array The array of leaves
|
|
*
|
|
* @return void
|
|
*/
|
|
function print_leaves(array $array) : void {
|
|
foreach ($array as $key => $line) {
|
|
if (!is_array($line)) {
|
|
if (CACTI_CLI) {
|
|
print $line . PHP_EOL;
|
|
} else {
|
|
print '<tr><td>' . $line . '</td></tr>';
|
|
}
|
|
} else {
|
|
if ($key === 'tune') {
|
|
continue;
|
|
}
|
|
|
|
if ($key === 'resize') {
|
|
continue;
|
|
}
|
|
print_leaves($line);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* rrdtool_tune - create rrdtool tune/resize commands html+cli enabled
|
|
*
|
|
* @param string $rrd_file The rrd file name
|
|
* @param array $diff The array of discrepancies between cacti settings and rrd file info
|
|
* @param bool $show_source The only show text+commands or execute all commands, execute is for cli mode only!
|
|
*
|
|
* @return void
|
|
*/
|
|
function rrdtool_tune(string $rrd_file, array $diff, bool $show_source = true) : void {
|
|
$rrd_path = read_config_option('path_rrdtool');
|
|
|
|
$cmd = [];
|
|
|
|
if ($show_source && cacti_sizeof($diff)) {
|
|
if (!CACTI_CLI) {
|
|
html_header([__('Errors Found')]);
|
|
}
|
|
|
|
print_leaves($diff);
|
|
|
|
if (!CACTI_CLI) {
|
|
html_end_box();
|
|
}
|
|
}
|
|
|
|
if (isset($diff['tune']) || isset($diff['resize'])) {
|
|
if (!CACTI_CLI) {
|
|
html_header([__('Optional Tuning Recommendations')]);
|
|
}
|
|
}
|
|
|
|
if (isset($diff['tune']) && cacti_sizeof($diff['tune'])) {
|
|
// create tune commands
|
|
foreach ($diff['tune'] as $line) {
|
|
if ($show_source == true) {
|
|
if (CACTI_CLI) {
|
|
print $rrd_path . ' tune ' . $line . PHP_EOL;
|
|
} else {
|
|
print '<tr><td>' . $rrd_path . ' tune ' . $line . '</td></tr>';
|
|
}
|
|
} else {
|
|
rrdtool_execute("tune $line", true, RRDTOOL_OUTPUT_STDOUT);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($diff['resize']) && cacti_sizeof($diff['resize'])) {
|
|
// each resize goes into an extra line
|
|
foreach ($diff['resize'] as $line) {
|
|
if ($show_source == true) {
|
|
if (CACTI_CLI) {
|
|
print read_config_option('path_rrdtool') . ' resize ' . $line . PHP_EOL;
|
|
print __('rename %s to %s', dirname($rrd_file) . '/resize.rrd', $rrd_file) . PHP_EOL;
|
|
} else {
|
|
print '<tr><td>' . read_config_option('path_rrdtool') . ' resize ' . $line . '</td></tr>';
|
|
print '<tr><td>' . __('rename %s to %s', dirname($rrd_file) . '/resize.rrd', $rrd_file) . '</td></tr>';
|
|
}
|
|
} else {
|
|
rrdtool_execute("resize $line", true, RRDTOOL_OUTPUT_STDOUT);
|
|
rename(dirname($rrd_file) . '/resize.rrd', $rrd_file);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($diff['tune']) || isset($diff['resize'])) {
|
|
if (!CACTI_CLI) {
|
|
html_end_box();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* rrd_check - Given a data source id, check the rrdtool file to the data source definition
|
|
*
|
|
* @param int $data_source_id The data source id
|
|
*
|
|
* @return void An array containing issues with the rrdtool file definition vs data source
|
|
*/
|
|
function rrd_check(int $data_source_id) : void {
|
|
global $rrd_tune_array, $data_source_types;
|
|
|
|
$data_source_name = get_data_source_item_name($rrd_tune_array['data_source_id']);
|
|
$data_source_type = $data_source_types[$rrd_tune_array['data-source-type']];
|
|
$data_source_path = get_data_source_path($rrd_tune_array['data_source_id'], true);
|
|
}
|
|
|
|
/**
|
|
* rrd_repair - Given a data source id, update the rrdtool file to match the data source definition
|
|
*
|
|
* @param int $data_source_id The data source id
|
|
*
|
|
* @return void
|
|
*/
|
|
function rrd_repair(int $data_source_id) : void {
|
|
global $rrd_tune_array, $data_source_types;
|
|
|
|
$data_source_name = get_data_source_item_name($rrd_tune_array['data_source_id']);
|
|
$data_source_type = $data_source_types[$rrd_tune_array['data-source-type']];
|
|
$data_source_path = get_data_source_path($rrd_tune_array['data_source_id'], true);
|
|
}
|
|
|
|
/**
|
|
* Load an RRDtool dump as XML without network/entity access or PHP warnings.
|
|
*
|
|
* @param string $file Existing RRD file
|
|
* @param mixed $pipe Optional RRDtool pipe
|
|
* @param callable|null $executor Test seam for RRDtool execution
|
|
*
|
|
* @return DOMDocument|false Parsed dump, or false when execution/parsing fails
|
|
*/
|
|
function rrdtool_dump_document(string $file, mixed $pipe = null, ?callable $executor = null) : DOMDocument|false {
|
|
$executor ??= 'rrdtool_execute';
|
|
$output = $executor(
|
|
'dump ' . cacti_escapeshellarg($file),
|
|
false,
|
|
RRDTOOL_OUTPUT_STDOUT,
|
|
$pipe,
|
|
'UTIL'
|
|
);
|
|
|
|
if (!is_string($output) || trim($output) === '') {
|
|
return false;
|
|
}
|
|
|
|
$dom = new DOMDocument();
|
|
$previous = libxml_use_internal_errors(true);
|
|
|
|
try {
|
|
return $dom->loadXML($output, LIBXML_NONET) ? $dom : false;
|
|
} finally {
|
|
libxml_clear_errors();
|
|
libxml_use_internal_errors($previous);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore a modified RRD XML document to a validated replacement file.
|
|
*
|
|
* The original RRD remains untouched until RRDtool has produced a non-empty
|
|
* replacement in a private sibling directory. The final rename is atomic on
|
|
* supported local filesystems, and every temporary artifact is removed on
|
|
* both success and failure.
|
|
*
|
|
* @param DOMDocument $dom Modified RRDtool XML document
|
|
* @param string $file Existing RRD file to replace
|
|
* @param mixed $pipe Optional RRDtool pipe
|
|
* @param callable|null $executor Test seam for RRDtool execution
|
|
* @param callable|null $renamer Test seam for the final atomic rename
|
|
*
|
|
* @return bool Whether a validated replacement was installed
|
|
*/
|
|
function rrdtool_restore_document(DOMDocument $dom, string $file, mixed $pipe = null, ?callable $executor = null, ?callable $renamer = null) : bool {
|
|
$directory = realpath(dirname($file));
|
|
$metadata = @stat($file);
|
|
|
|
if ($directory === false || !is_array($metadata) || !is_writable($file) || !is_writable($directory)) {
|
|
return false;
|
|
}
|
|
|
|
$placeholder = tempnam($directory, '.cacti-rrd-restore-');
|
|
|
|
if ($placeholder === false || !unlink($placeholder) || !mkdir($placeholder, 0700)) {
|
|
return false;
|
|
}
|
|
|
|
$xml_file = $placeholder . DIRECTORY_SEPARATOR . 'source.xml';
|
|
$new_file = $placeholder . DIRECTORY_SEPARATOR . 'replacement.rrd';
|
|
$executor ??= 'rrdtool_execute';
|
|
$renamer ??= 'rename';
|
|
|
|
try {
|
|
$bytes = $dom->save($xml_file);
|
|
|
|
if ($bytes === false || $bytes < 1 || !chmod($xml_file, 0600)) {
|
|
return false;
|
|
}
|
|
|
|
$result = $executor(
|
|
'restore -f ' . cacti_escapeshellarg($xml_file) . ' ' . cacti_escapeshellarg($new_file),
|
|
false,
|
|
RRDTOOL_OUTPUT_STDOUT,
|
|
$pipe,
|
|
'UTIL'
|
|
);
|
|
|
|
if ($result === false || !is_file($new_file) || filesize($new_file) < 1) {
|
|
return false;
|
|
}
|
|
|
|
if (!chmod($new_file, $metadata['mode'] & 0777)) {
|
|
return false;
|
|
}
|
|
|
|
if (function_exists('posix_geteuid') && posix_geteuid() === 0) {
|
|
if (!chown($new_file, $metadata['uid']) || !chgrp($new_file, $metadata['gid'])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return $renamer($new_file, $file) === true;
|
|
} finally {
|
|
if (is_file($xml_file)) {
|
|
unlink($xml_file);
|
|
}
|
|
|
|
if (is_file($new_file)) {
|
|
unlink($new_file);
|
|
}
|
|
|
|
if (is_dir($placeholder)) {
|
|
rmdir($placeholder);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* rrd_datasource_add - add a (list of) datasource(s) to an (array of) rrd file(s)
|
|
*
|
|
* @param array $file_array The array of rrd files
|
|
* @param array $ds_array The array of datasource parameters
|
|
* @param bool $debug Debug mode
|
|
*
|
|
* @return mixed Success or error message
|
|
*/
|
|
function rrd_datasource_add(array $file_array, array $ds_array, bool $debug) : mixed {
|
|
global $data_source_types, $consolidation_functions;
|
|
|
|
$rrdtool_pipe = rrd_init();
|
|
|
|
try {
|
|
// iterate all given rrd files
|
|
foreach ($file_array as $file) {
|
|
$dom = rrdtool_dump_document($file, $rrdtool_pipe);
|
|
|
|
if (!$dom instanceof DOMDocument) {
|
|
$check['err_msg'] = __('Error while parsing the XML of rrdtool dump');
|
|
|
|
return $check;
|
|
}
|
|
|
|
/* rrdtool dump depends on rrd file version:
|
|
* version 0001 => RRDtool 1.0.x
|
|
* version 0003 => RRDtool 1.2.x, 1.3.x, 1.4.x, 1.5.x, 1.6.x
|
|
*/
|
|
$version_node = $dom->getElementsByTagName('version')->item(0);
|
|
$version = $version_node !== null ? trim($version_node->nodeValue) : RRD_FILE_VERSION3;
|
|
|
|
// now start XML processing
|
|
foreach ($ds_array as $ds) {
|
|
// first, append the <DS> structure in the rrd header
|
|
if ($ds['type'] === $data_source_types[5]) {
|
|
rrd_append_compute_ds($dom, $version, $ds['name'], $ds['type'], $ds['cdef']);
|
|
} else {
|
|
rrd_append_ds($dom, $version, $ds['name'], $ds['type'], $ds['heartbeat'], $ds['min'], $ds['max']);
|
|
}
|
|
// now work on the <DS> structure as part of the <cdp_prep> tree
|
|
rrd_append_cdp_prep_ds($dom, $version);
|
|
// add <V>alues to the <database> tree
|
|
rrd_append_value($dom);
|
|
}
|
|
|
|
if ($debug) {
|
|
print $dom->saveXML();
|
|
} elseif (!rrdtool_restore_document($dom, $file, $rrdtool_pipe)) {
|
|
$check['err_msg'] = __('ERROR: Unable to install validated RRDfile replacement for %s', $file);
|
|
|
|
return $check;
|
|
} else {
|
|
cacti_log('Added Data Source(s) to RRDfile: ' . $file, false, 'UTIL');
|
|
}
|
|
}
|
|
} finally {
|
|
rrd_close($rrdtool_pipe);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* rrd_rra_delete - delete a (list of) rra(s) from an (array of) rrd file(s)
|
|
*
|
|
* @param array $file_array An array of rrd files
|
|
* @param array $rra_array An array of rra parameters
|
|
* @param bool $debug Debug mode
|
|
*
|
|
* @return mixed True for success (bool) or error message (array)
|
|
*/
|
|
function rrd_rra_delete(array $file_array, array $rra_array, bool $debug) : mixed {
|
|
$rrdtool_pipe = rrd_init();
|
|
|
|
try {
|
|
// iterate all given rrd files
|
|
foreach ($file_array as $file) {
|
|
$dom = rrdtool_dump_document($file, $rrdtool_pipe);
|
|
|
|
if (!$dom instanceof DOMDocument) {
|
|
$check['err_msg'] = __('Error while parsing the XML of RRDtool dump');
|
|
|
|
return $check;
|
|
}
|
|
|
|
// now start XML processing
|
|
foreach ($rra_array as $rra) {
|
|
rrd_delete_rra($dom, $rra);
|
|
}
|
|
|
|
if ($debug) {
|
|
print $dom->saveXML();
|
|
} elseif (!rrdtool_restore_document($dom, $file, $rrdtool_pipe)) {
|
|
$check['err_msg'] = __('ERROR: Unable to install validated RRDfile replacement for %s', $file);
|
|
|
|
return $check;
|
|
} else {
|
|
cacti_log('Deleted RRA(s) from RRDfile: ' . $file, false, 'UTIL');
|
|
}
|
|
}
|
|
} finally {
|
|
rrd_close($rrdtool_pipe);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* rrd_rra_clone - clone a (list of) rra(s) from an (array of) rrd file(s)
|
|
*
|
|
* @param array $file_array And array of rrd files
|
|
* @param string $cf The new consolidation function
|
|
* @param array $rra_array An array of rra parameters
|
|
* @param bool $debug Debug mode
|
|
*
|
|
* @return mixed Success (bool) or error message (array)
|
|
*/
|
|
function rrd_rra_clone(array $file_array, string $cf, array $rra_array, bool $debug) : mixed {
|
|
$rrdtool_pipe = rrd_init();
|
|
|
|
try {
|
|
// iterate all given rrd files
|
|
foreach ($file_array as $file) {
|
|
$dom = rrdtool_dump_document($file, $rrdtool_pipe);
|
|
|
|
if (!$dom instanceof DOMDocument) {
|
|
$check['err_msg'] = __('Error while parsing the XML of RRDtool dump');
|
|
|
|
return $check;
|
|
}
|
|
|
|
// now start XML processing
|
|
foreach ($rra_array as $rra) {
|
|
rrd_copy_rra($dom, $cf, $rra);
|
|
}
|
|
|
|
if ($debug) {
|
|
print $dom->saveXML();
|
|
} elseif (!rrdtool_restore_document($dom, $file, $rrdtool_pipe)) {
|
|
$check['err_msg'] = __('ERROR: Unable to install validated RRDfile replacement for %s', $file);
|
|
|
|
return $check;
|
|
} else {
|
|
cacti_log('Cloned RRA(s) in RRDfile: ' . $file, false, 'UTIL');
|
|
}
|
|
}
|
|
} finally {
|
|
rrd_close($rrdtool_pipe);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* rrd_append_ds - appends a <DS> subtree to an RRD XML structure
|
|
*
|
|
* @param object $dom The DOM object, where the RRD XML is stored
|
|
* @param string $version The rrd file version
|
|
* @param string $name Name of the new ds
|
|
* @param string $type Type of the new ds
|
|
* @param int $min_hb Heartbeat of the new ds
|
|
* @param string $min Min value of the new ds or [NaN|U]
|
|
* @param string $max Max value of the new ds or [NaN|U]
|
|
*
|
|
* @return object Modified DOM
|
|
*/
|
|
function rrd_append_ds(object $dom, string $version, string $name, string $type, int $min_hb, string $min, string $max) : object {
|
|
// rrdtool version dependencies
|
|
if ($version === RRD_FILE_VERSION1) {
|
|
$last_ds = 'U';
|
|
} elseif ($version === RRD_FILE_VERSION3) {
|
|
$last_ds = 'UNKN';
|
|
} else {
|
|
$last_ds = 'UNKN';
|
|
}
|
|
|
|
// create <DS> subtree
|
|
$new_dom = new DOMDocument;
|
|
// pretty print
|
|
$new_dom->formatOutput = true;
|
|
// this defines the new node structure
|
|
$new_dom->loadXML("
|
|
<ds>
|
|
<name> $name </name>
|
|
<type> $type </type>
|
|
<minimal_heartbeat> $min_hb </minimal_heartbeat>
|
|
<min> $min </min>
|
|
<max> $max </max>
|
|
|
|
<!-- PDP Status -->
|
|
<last_ds> $last_ds </last_ds>
|
|
<value> 0.0000000000e+00 </value>
|
|
<unknown_sec> 0 </unknown_sec>
|
|
</ds>");
|
|
|
|
// create a node element from new document
|
|
$new_node = $new_dom->getElementsByTagName('ds')->item(0);
|
|
// echo $new_dom->saveXML(); # print new node
|
|
|
|
// get XPATH notation required for positioning
|
|
// $xpath = new DOMXPath($dom);
|
|
/* get XPATH for entry where new node will be inserted
|
|
* which is the <rra> entry */
|
|
// $insert = $xpath->query('/rrd/rra')->item(0);
|
|
$insert = $dom->getElementsByTagName('rra')->item(0);
|
|
|
|
if ($insert !== null && $new_node !== null) {
|
|
// import the new node
|
|
$new_node = $dom->importNode($new_node, true);
|
|
// and insert it at the correct place
|
|
$insert->parentNode->insertBefore($new_node, $insert);
|
|
}
|
|
|
|
return $dom;
|
|
}
|
|
|
|
/**
|
|
* rrd_append_compute_ds - COMPUTE DS: appends a <DS> subtree to an RRD XML structure
|
|
*
|
|
* @param object $dom The DOM object, where the RRD XML is stored
|
|
* @param string $version The rrd file version
|
|
* @param string $name Name of the new ds
|
|
* @param string $type Type of the new ds
|
|
* @param int $cdef The cdef rpn used for COMPUTE
|
|
*
|
|
* @return object Modified DOM
|
|
*/
|
|
function rrd_append_compute_ds(object $dom, string $version, string $name, string $type, int $cdef) : object {
|
|
// rrdtool version dependencies
|
|
if ($version === RRD_FILE_VERSION1) {
|
|
$last_ds = 'U';
|
|
} elseif ($version === RRD_FILE_VERSION3) {
|
|
$last_ds = 'UNKN';
|
|
} else {
|
|
$last_ds = 'UNKN';
|
|
}
|
|
|
|
// create <DS> subtree
|
|
$new_dom = new DOMDocument;
|
|
// pretty print
|
|
$new_dom->formatOutput = true;
|
|
// this defines the new node structure
|
|
$new_dom->loadXML("
|
|
<ds>
|
|
<name> $name </name>
|
|
<type> $type </type>
|
|
<cdef> $cdef </cdef>
|
|
|
|
<!-- PDP Status -->
|
|
<last_ds> $last_ds </last_ds>
|
|
<value> 0.0000000000e+00 </value>
|
|
<unknown_sec> 0 </unknown_sec>
|
|
</ds>");
|
|
|
|
// create a node element from new document
|
|
$new_node = $new_dom->getElementsByTagName('ds')->item(0);
|
|
|
|
// get XPATH notation required for positioning
|
|
// $xpath = new DOMXPath($dom);
|
|
/* get XPATH for entry where new node will be inserted
|
|
* which is the <rra> entry */
|
|
// $insert = $xpath->query('/rrd/rra')->item(0);
|
|
$insert = $dom->getElementsByTagName('rra')->item(0);
|
|
|
|
if ($insert !== null && $new_node !== null) {
|
|
// import the new node
|
|
$new_node = $dom->importNode($new_node, true);
|
|
// and insert it at the correct place
|
|
$insert->parentNode->insertBefore($new_node, $insert);
|
|
}
|
|
|
|
return $dom;
|
|
}
|
|
|
|
/**
|
|
* rrd_append_cdp_prep_ds - append a <DS> subtree to the <CDP_PREP> subtrees of a RRD XML structure
|
|
*
|
|
* @param object $dom The DOM object, where the RRD XML is stored
|
|
* @param string $version The rrd file version
|
|
*
|
|
* @return object The modified DOM object
|
|
*/
|
|
function rrd_append_cdp_prep_ds(object $dom, string $version) : object {
|
|
// get all <cdp_prep><ds> entries
|
|
// $cdp_prep_list = $xpath->query('/rrd/rra/cdp_prep');
|
|
$rra_node = $dom->getElementsByTagName('rra')->item(0);
|
|
|
|
if ($rra_node === null) {
|
|
return $dom;
|
|
}
|
|
|
|
$cdp_prep_list = $rra_node->getElementsByTagName('cdp_prep');
|
|
|
|
// get XPATH notation required for positioning
|
|
// $xpath = new DOMXPath($dom);
|
|
|
|
// get XPATH for source <ds> entry
|
|
// $src_ds = $xpath->query('/rrd/rra/cdp_prep/ds')->item(0);
|
|
$cdp_prep_first = $cdp_prep_list->item(0);
|
|
|
|
if ($cdp_prep_first === null) {
|
|
return $dom;
|
|
}
|
|
|
|
$src_ds = $cdp_prep_first->getElementsByTagName('ds')->item(0);
|
|
|
|
if ($src_ds === null) {
|
|
return $dom;
|
|
}
|
|
|
|
// clone the source ds entry to preserve RRDtool notation
|
|
$new_ds = $src_ds->cloneNode(true);
|
|
|
|
// rrdtool version dependencies
|
|
if ($version === RRD_FILE_VERSION3) {
|
|
$new_ds->getElementsByTagName('primary_value')->item(0)->nodeValue = ' NaN ';
|
|
$new_ds->getElementsByTagName('secondary_value')->item(0)->nodeValue = ' NaN ';
|
|
}
|
|
|
|
// the new node always has default entries
|
|
$new_ds->getElementsByTagName('value')->item(0)->nodeValue = ' NaN ';
|
|
$new_ds->getElementsByTagName('unknown_datapoints')->item(0)->nodeValue = ' 0 ';
|
|
|
|
// iterate all entries found, equals 'number of <rra>' times 'number of <ds>'
|
|
if ($cdp_prep_list->length) {
|
|
foreach ($cdp_prep_list as $cdp_prep) {
|
|
/* $cdp_prep now points to the next <cdp_prep> XML Element
|
|
* and append new ds entry at end of <cdp_prep> child list */
|
|
$cdp_prep->appendChild($new_ds->cloneNode(true));
|
|
}
|
|
}
|
|
|
|
return $dom;
|
|
}
|
|
|
|
/**
|
|
* rrd_append_value - append a <V>alue element to the <DATABASE> subtrees of a RRD XML structure
|
|
*
|
|
* @param object $dom The DOM object, where the RRD XML is stored
|
|
*
|
|
* @return object The modified DOM object
|
|
*/
|
|
function rrd_append_value(object $dom) : object {
|
|
if (!$dom instanceof DOMDocument) {
|
|
return $dom;
|
|
}
|
|
|
|
// get XPATH notation required for positioning
|
|
// $xpath = new DOMXPath($dom);
|
|
|
|
// get all <cdp_prep><ds> entries
|
|
// $itemList = $xpath->query('/rrd/rra/database/row');
|
|
$itemList = $dom->getElementsByTagName('row');
|
|
|
|
// create <V> entry to preserve RRDtool notation
|
|
$new_v = $dom->createElement('v', ' NaN ');
|
|
|
|
// iterate all entries found, equals 'number of <rra>' times 'number of <ds>'
|
|
if ($itemList->length) {
|
|
foreach ($itemList as $item) {
|
|
/* $item now points to the next <row> XML Element
|
|
* and append new v entry at end of <row> child list */
|
|
$item->appendChild($new_v->cloneNode(true));
|
|
}
|
|
}
|
|
|
|
return $dom;
|
|
}
|
|
|
|
/**
|
|
* rrd_delete_rra - delete an <RRA> subtree from the <RRD> XML structure
|
|
*
|
|
* @param object $dom The DOM document, where the RRD XML is stored
|
|
* @param array $rra_parm A single rra parameter set, given by the user
|
|
*
|
|
* @return object The modified DOM object
|
|
*/
|
|
function rrd_delete_rra(object $dom, array $rra_parm) : object {
|
|
if (!$dom instanceof DOMDocument) {
|
|
return $dom;
|
|
}
|
|
|
|
// find all RRA DOMNodes
|
|
$rras = $dom->getElementsByTagName('rra');
|
|
|
|
// iterate all entries found
|
|
$nb = $rras->length;
|
|
|
|
for ($pos = 0; $pos < $nb; $pos++) {
|
|
// retrieve all RRA DOMNodes one by one
|
|
$rra = $rras->item($pos);
|
|
$cf = $rra->getElementsByTagName('cf')->item(0)->nodeValue;
|
|
$pdp_per_row = $rra->getElementsByTagName('pdp_per_row')->item(0)->nodeValue;
|
|
$xff = $rra->getElementsByTagName('xff')->item(0)->nodeValue;
|
|
$rows = $rra->getElementsByTagName('row')->length;
|
|
|
|
if ($cf == $rra_parm['cf'] &&
|
|
$pdp_per_row == $rra_parm['pdp_per_row'] &&
|
|
$xff == $rra_parm['xff'] &&
|
|
$rows == $rra_parm['rows']) {
|
|
print (__('RRA (CF=%s, ROWS=%d, PDP_PER_ROW=%d, XFF=%1.2f) removed from RRD file', $cf, $rows, $pdp_per_row, $xff)) . PHP_EOL;
|
|
// we need the parentNode for removal operation
|
|
$parent = $rra->parentNode;
|
|
$parent->removeChild($rra);
|
|
|
|
break; // do NOT accidentally remove more than one element, else loop back to forth
|
|
}
|
|
}
|
|
|
|
return $dom;
|
|
}
|
|
|
|
/**
|
|
* rrd_copy_rra - clone an <RRA> subtree of the <RRD> XML structure, replacing cf
|
|
*
|
|
* @param object $dom The DOM document, where the RRD XML is stored
|
|
* @param string $cf New consolidation function
|
|
* @param array $rra_parm A single rra parameter set, given by the user
|
|
*
|
|
* @return object The modified DOM object
|
|
*/
|
|
function rrd_copy_rra(object $dom, string $cf, array $rra_parm) : object {
|
|
if (!$dom instanceof DOMDocument) {
|
|
return $dom;
|
|
}
|
|
|
|
// find all RRA DOMNodes
|
|
$rras = $dom->getElementsByTagName('rra');
|
|
|
|
// iterate all entries found
|
|
$nb = $rras->length;
|
|
|
|
for ($pos = 0; $pos < $nb; $pos++) {
|
|
// retrieve all RRA DOMNodes one by one
|
|
$rra = $rras->item($pos);
|
|
$_cf = $rra->getElementsByTagName('cf')->item(0)->nodeValue;
|
|
$_pdp_per_row = $rra->getElementsByTagName('pdp_per_row')->item(0)->nodeValue;
|
|
$_xff = $rra->getElementsByTagName('xff')->item(0)->nodeValue;
|
|
$_rows = $rra->getElementsByTagName('row')->length;
|
|
|
|
if ($_cf == $rra_parm['cf'] &&
|
|
$_pdp_per_row == $rra_parm['pdp_per_row'] &&
|
|
$_xff == $rra_parm['xff'] &&
|
|
$_rows == $rra_parm['rows']) {
|
|
print (__('RRA (CF=%s, ROWS=%d, PDP_PER_ROW=%d, XFF=%1.2f) adding to RRD file', $cf, $_rows, $_pdp_per_row, $_xff)) . PHP_EOL;
|
|
// we need the parentNode for append operation
|
|
$parent = $rra->parentNode;
|
|
|
|
// get a clone of the matching RRA
|
|
$new_rra = $rra->cloneNode(true);
|
|
|
|
// and find the 'old' cf
|
|
// $old_cf = $new_rra->getElementsByTagName('cf')->item(0);
|
|
// now replace old cf with new one
|
|
// $old_cf->childNodes->item(0)->replaceData(0,20,$cf);
|
|
if ($new_rra instanceof DOMElement) {
|
|
$new_rra->getElementsByTagName('cf')->item(0)->nodeValue = $cf;
|
|
}
|
|
|
|
// append new rra entry at end of the list
|
|
$parent->appendChild($new_rra);
|
|
|
|
break; // do NOT accidentally clone more than one element, else loop back to forth
|
|
}
|
|
}
|
|
|
|
return $dom;
|
|
}
|
|
|
|
function rrdtool_replacement_legend(int $local_graph_id) : array {
|
|
$graph_data = db_fetch_assoc_prepared('SELECT gti.legend, dtr.data_source_name,
|
|
gti.consolidation_function_id, c.hex, gti.alpha, gti.text_format
|
|
FROM graph_templates_item AS gti
|
|
INNER JOIN data_template_rrd AS dtr
|
|
ON gti.task_item_id = dtr.id
|
|
LEFT JOIN colors AS c
|
|
ON gti.color_id = c.id
|
|
WHERE gti.local_graph_id = ?
|
|
AND graph_type_id IN (7,8,2,4,5,6,20)
|
|
ORDER BY gti.sequence',
|
|
[$local_graph_id]);
|
|
|
|
$is_agg = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM aggregate_graphs
|
|
WHERE local_graph_id = ?',
|
|
[$local_graph_id]);
|
|
|
|
$legend = [];
|
|
|
|
if (cacti_sizeof($graph_data)) {
|
|
foreach ($graph_data as $g) {
|
|
if ($is_agg && $g['text_format'] != '') {
|
|
$agg_prefix = $g['text_format'] . ' - ';
|
|
} else {
|
|
$agg_prefix = '';
|
|
}
|
|
|
|
if ($g['legend'] != '') {
|
|
$legend[] = ['legend' => $agg_prefix . $g['legend'], 'color' => '#' . $g['hex'] . $g['alpha']];
|
|
} else {
|
|
if ($g['data_source_name'] == '') {
|
|
$g['data_source_name'] = 'Unknown';
|
|
}
|
|
|
|
$g['data_source_name'] = ucwords(str_replace('_', ' ', $g['data_source_name']));
|
|
|
|
switch($g['consolidation_function_id']) {
|
|
case 1:
|
|
$g['data_source_name'] .= ' (AVG)';
|
|
|
|
break;
|
|
case 2:
|
|
$g['data_source_name'] .= ' (MIN)';
|
|
|
|
break;
|
|
case 3:
|
|
$g['data_source_name'] .= ' (MAX)';
|
|
|
|
break;
|
|
case 4:
|
|
$g['data_source_name'] .= ' (LAST)';
|
|
|
|
break;
|
|
}
|
|
|
|
$legend[] = ['legend' => $agg_prefix . $g['data_source_name'], 'color' => '#' . $g['hex'] . $g['alpha']];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $legend;
|
|
}
|
|
|
|
function rrdtool_parse_error(string $string) : string {
|
|
if (preg_match('/ERROR. opening \'(.*)\': (No such|Permiss).*/', $string, $matches)) {
|
|
if (cacti_sizeof($matches) >= 2) {
|
|
$filename = (string) $matches[1];
|
|
$rra_name = basename($filename);
|
|
$rra_path = dirname($filename) . '/';
|
|
|
|
if (!is_resource_writable($rra_path)) {
|
|
$message = __('Website does not have write access to %s, may be unable to create/update RRDs', 'folder');
|
|
$rra_name = str_replace(CACTI_PATH_RRA . '/', '', $rra_path);
|
|
$rra_path = '';
|
|
} else {
|
|
$found = stripos($filename, CACTI_PATH_BASE);
|
|
|
|
if ($found > 0 || $found === 0) {
|
|
$rra_file = str_replace(CACTI_PATH_RRA . '/', '', $filename);
|
|
$rra_name = basename($rra_file);
|
|
$rra_path = dirname($rra_file);
|
|
} else {
|
|
$rra_name = basename($filename);
|
|
$rra_path = __('(Custom)');
|
|
}
|
|
|
|
if (!is_resource_writable($filename)) {
|
|
$message = __('Website does not have write access to %s, may be unable to create/update RRDs', 'data file');
|
|
} else {
|
|
$message = __('Failed to open data file, poller may not have run yet');
|
|
}
|
|
|
|
$rra_path = '(' . __('RRA Folder') . ': ' . ((empty($rra_path) || $rra_path == '.') ? __('Root') : $rra_path) . ')';
|
|
}
|
|
|
|
$string = $message . ":\n\0x27\n" . $rra_name;
|
|
|
|
if (!empty($rra_path)) {
|
|
$string .= "\n" . $rra_path;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $string;
|
|
}
|
|
|
|
function rrdtool_create_error_image(string $string, mixed $width = '', mixed $height = '') : string {
|
|
global $dejavu_paths;
|
|
|
|
$string = rrdtool_parse_error($string);
|
|
|
|
// put image in buffer
|
|
ob_start();
|
|
|
|
$image_data = false;
|
|
$font_color = '000000';
|
|
$font_size = 8;
|
|
$back_color = 'F3F3F3';
|
|
$shadea = 'CBCBCB';
|
|
$shadeb = '999999';
|
|
|
|
if (CACTI_SERVER_OS == 'unix') {
|
|
foreach ($dejavu_paths as $dejavupath) {
|
|
if (file_exists($dejavupath . '/DejaVuSans.ttf')) {
|
|
$font_file = $dejavupath . '/DejaVuSans.ttf';
|
|
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
$font_file = 'C:/Windows/Fonts/Arial.ttf';
|
|
}
|
|
|
|
$themefile = CACTI_PATH_INCLUDE . '/themes/' . get_selected_theme() . '/rrdtheme.php';
|
|
|
|
if (file_exists($themefile) && is_readable($themefile)) {
|
|
include($themefile);
|
|
|
|
if (isset($rrdfonts['legend']['size'])) {
|
|
$font_size = $rrdfonts['legend']['size'];
|
|
}
|
|
|
|
if (isset($rrdcolors['font'])) {
|
|
$font_color = $rrdcolors['font'];
|
|
}
|
|
|
|
if (isset($rrdcolors['canvas'])) {
|
|
$back_color = $rrdcolors['canvas'];
|
|
}
|
|
|
|
if (isset($rrdcolors['shadea'])) {
|
|
$shadea = $rrdcolors['shadea'];
|
|
}
|
|
|
|
if (isset($rrdcolors['shadeb'])) {
|
|
$shadeb = $rrdcolors['shadeb'];
|
|
}
|
|
}
|
|
|
|
$image = imagecreatetruecolor(450, 200);
|
|
imagesavealpha($image, true);
|
|
|
|
// create a transparent color
|
|
$transparent = imagecolorallocatealpha($image, 0, 0, 0, 127);
|
|
|
|
if ($transparent !== false) {
|
|
imagefill($image, 0, 0, $transparent);
|
|
}
|
|
|
|
// background the entire image with the frame
|
|
$shadeb_parsed = sscanf($shadeb, '%02x%02x%02x');
|
|
$red = min(255, max(0, (int) ($shadeb_parsed[0] ?? 0)));
|
|
$green = min(255, max(0, (int) ($shadeb_parsed[1] ?? 0)));
|
|
$blue = min(255, max(0, (int) ($shadeb_parsed[2] ?? 0)));
|
|
|
|
$shadeb_color = imagecolorallocate($image, $red, $green, $blue);
|
|
|
|
if ($shadeb_color !== false) {
|
|
imagefill($image, 0, 0, $shadeb_color);
|
|
}
|
|
|
|
// set the background color
|
|
$shadea_parsed = sscanf($shadea, '%02x%02x%02x');
|
|
$red = min(255, max(0, (int) ($shadea_parsed[0] ?? 0)));
|
|
$green = min(255, max(0, (int) ($shadea_parsed[1] ?? 0)));
|
|
$blue = min(255, max(0, (int) ($shadea_parsed[2] ?? 0)));
|
|
|
|
$shadea_color = imagecolorallocate($image, $red, $green, $blue);
|
|
|
|
if ($shadea_color !== false) {
|
|
imagefilledrectangle($image, 1, 1, 448, 198, $shadea_color);
|
|
}
|
|
|
|
// set the background color
|
|
$back_parsed = sscanf($back_color, '%02x%02x%02x');
|
|
$red = min(255, max(0, (int) ($back_parsed[0] ?? 0)));
|
|
$green = min(255, max(0, (int) ($back_parsed[1] ?? 0)));
|
|
$blue = min(255, max(0, (int) ($back_parsed[2] ?? 0)));
|
|
|
|
$back_color_alloc = imagecolorallocate($image, $red, $green, $blue);
|
|
|
|
if ($back_color_alloc !== false) {
|
|
imagefilledrectangle($image, 2, 2, 447, 197, $back_color_alloc);
|
|
}
|
|
|
|
// allocate the image
|
|
$logo = imagecreatefrompng(CACTI_PATH_IMAGES . '/cacti_error_image.png');
|
|
|
|
// merge the two images
|
|
if ($logo !== false) {
|
|
imagecopy($image, $logo, 0, 0, 0, 0, 450, 200);
|
|
}
|
|
|
|
// set the background color
|
|
$font_parsed = sscanf($font_color, '%02x%02x%02x');
|
|
$red = min(255, max(0, (int) ($font_parsed[0] ?? 0)));
|
|
$green = min(255, max(0, (int) ($font_parsed[1] ?? 0)));
|
|
$blue = min(255, max(0, (int) ($font_parsed[2] ?? 0)));
|
|
|
|
$text_color = imagecolorallocate($image, $red, $green, $blue);
|
|
|
|
// see the size of the string
|
|
$string = trim($string);
|
|
$maxstring = intval(ceil((450 - (125 + 10)) / ($font_size / 0.9)));
|
|
$stringlen = strlen($string) * $font_size;
|
|
$padding = 5;
|
|
|
|
if ($stringlen > $maxstring) {
|
|
$cstring = wordwrap($string, $maxstring, "\n", true);
|
|
$strings = explode("\n", $cstring);
|
|
$strings = array_reverse($strings);
|
|
$lines = cacti_sizeof($strings);
|
|
} elseif (strlen(trim($string)) == 0) {
|
|
$strings = [__('Unknown RRDtool Error')];
|
|
$lines = 1;
|
|
} else {
|
|
$strings = [$string];
|
|
$lines = 1;
|
|
}
|
|
|
|
// setup the text position, image is 450x200, we start at 125 pixels from the left
|
|
$xpos = 125;
|
|
$texth = ($lines * $font_size + (($lines - 1) * $padding));
|
|
$ypos = round((200 / 2) + ($texth / 2),0);
|
|
|
|
if ($text_color === false) {
|
|
$text_color = 0;
|
|
}
|
|
|
|
// set the font of the image
|
|
if (isset($font_file) && file_exists($font_file) && is_readable($font_file) && function_exists('imagettftext')) {
|
|
foreach ($strings as $string) {
|
|
if (trim($string) != '') {
|
|
if (@imagettftext($image, $font_size, 0, $xpos, intval($ypos), $text_color, $font_file, $string) === false) {
|
|
cacti_log('TTF text overlay failed');
|
|
}
|
|
$ypos -= ($font_size + $padding);
|
|
}
|
|
}
|
|
} else {
|
|
foreach ($strings as $string) {
|
|
if (trim($string) != '') {
|
|
if (@imagestring($image, $font_size, $xpos, intval($ypos), $string, $text_color) === false) {
|
|
cacti_log('Text overlay failed');
|
|
}
|
|
$ypos -= ($font_size + $padding);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($width != '' && $height != '') {
|
|
$nimage = imagecreatetruecolor($width, $height);
|
|
imagecopyresized($nimage, $image, 0, 0, 0, 0, $width, $height, 450, 200);
|
|
|
|
// create the image
|
|
imagepng($image);
|
|
} else {
|
|
// create the image
|
|
imagepng($image);
|
|
}
|
|
|
|
// get the image from the buffer
|
|
$image_data = ob_get_contents();
|
|
|
|
// flush the buffer
|
|
ob_end_clean();
|
|
|
|
return $image_data !== false ? $image_data : '';
|
|
}
|
|
|
|
/**
|
|
* gradient - Add gradient support for AREA type charts. This function adds several CDEF with different shading
|
|
*
|
|
* @param string $vname The data source name
|
|
* @param string $start_color The start color for the gradient
|
|
* @param string $end_color The end color for the gradient
|
|
* @param string $label Any label attached to it
|
|
* @param mixed $steps Defaults to 20
|
|
* @param mixed $lower Defaults to false
|
|
* @param string $alpha Alpha channel to be used
|
|
*
|
|
* @return string The additional CDEF/AREA command lines for rrdtool
|
|
*
|
|
* License: GPLv2
|
|
* Original Code: https://github.com/lingej/pnp4nagios/blob/master/share/pnp/application/helpers/rrd.php
|
|
*/
|
|
function gradient(string $vname = '', string $start_color = '#0000a0', string $end_color = '#f0f0f0', string $label = '',
|
|
mixed $steps = 20, mixed $lower = false, string $alpha = 'FF') : string {
|
|
if (preg_match('/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i', $start_color, $matches)) {
|
|
$r1 = hexdec($matches[1]);
|
|
$g1 = hexdec($matches[2]);
|
|
$b1 = hexdec($matches[3]);
|
|
} else {
|
|
$r1 = $g1 = $b1 = 0;
|
|
}
|
|
|
|
if (preg_match('/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i', $end_color, $matches)) {
|
|
$r2 = hexdec($matches[1]);
|
|
$g2 = hexdec($matches[2]);
|
|
$b2 = hexdec($matches[3]);
|
|
} else {
|
|
$r2 = $g2 = $b2 = 0;
|
|
}
|
|
|
|
$diff_r = $r2 - $r1;
|
|
$diff_g = $g2 - $g1;
|
|
$diff_b = $b2 - $b1;
|
|
$spline = '';
|
|
$spline_vname = 'var_' . substr(sha1((string) random_int(0, mt_getrandmax())), 1, 4);
|
|
$vnamet = $vname . substr(sha1((string) random_int(0, mt_getrandmax())), 1, 4);
|
|
|
|
if (preg_match('/^([0-9]{1,3})%$/', $lower, $matches)) {
|
|
$lower = $matches[1];
|
|
$spline .= sprintf('CDEF:%s_min=%s,100,/,%d,* ' . RRD_NL, $vnamet, $vname, $lower);
|
|
} elseif (preg_match('/^([0-9]+)$/', $lower, $matches)) {
|
|
$lower = $matches[1];
|
|
$spline .= sprintf('CDEF:%s_min=%s,%d,- ' . RRD_NL, $vnamet, $vname, $lower);
|
|
} else {
|
|
$lower = 0;
|
|
$spline .= sprintf('CDEF:%s_min=%s,%s,- ' . RRD_NL, $vnamet, $vname, $vname);
|
|
}
|
|
|
|
for ($i = $steps; $i > 0; $i--) {
|
|
$spline .= sprintf('CDEF:%s%d=%s,%s_min,-,%d,/,%d,*,%s_min,+ ' . RRD_NL, $spline_vname, $i, $vname, $vnamet, $steps, $i, $vnamet);
|
|
}
|
|
|
|
// We don't use alpha blending for the area right now
|
|
$alpha = 'FF';
|
|
|
|
for ($i = $steps; $i > 0; $i--) {
|
|
$factor = $i / $steps;
|
|
|
|
$r = round($r1 + $diff_r * $factor);
|
|
$g = round($g1 + $diff_g * $factor);
|
|
$b = round($b1 + $diff_b * $factor);
|
|
|
|
if ($i == $steps && $label != '' && strlen($label) > 2) {
|
|
$spline .= sprintf('AREA:%s%d#%02X%02X%02X%s:%s ' . RRD_NL, $spline_vname, $i, $r, $g, $b, $alpha, $label);
|
|
} else {
|
|
$spline .= sprintf('AREA:%s%d#%02X%02X%02X%s ' . RRD_NL, $spline_vname, $i, $r, $g, $b, $alpha);
|
|
}
|
|
}
|
|
|
|
$spline .= sprintf('AREA:%s%d#%02X%02X%02X%s ' . RRD_NL, $spline_vname, $steps, $r2, $g2, $b2, '00');
|
|
|
|
return $spline;
|
|
}
|
|
|
|
/**
|
|
* colourBrightness - Adjust the brightness of a hex color for gradient charts.
|
|
* Positive percent lightens; negative percent darkens.
|
|
*
|
|
* @param string $hex The hex representation of a color (with or without leading #)
|
|
* @param float $percent Brightness adjustment: decimal in [-1, 1] (e.g. 0.4 = +40%)
|
|
* or coerced integer in [-100, 100] (e.g. 40 = +40%). Values
|
|
* outside [-100, 100] are normalized then clamped to [-1, 1].
|
|
* NOTE: values in the open interval (1.0, 2.0) are treated as
|
|
* integers and divided by 100 (e.g. 1.5 → 0.015). The value
|
|
* 1.0 itself is NOT divided — it means 100% original color
|
|
* (the identity). There is no decimal representation of "1%
|
|
* brighter"; use 0.01 instead.
|
|
*
|
|
* @return string The adjusted color in the same format as the input
|
|
*
|
|
* License: GPLv2
|
|
* Original Code: http://www.barelyfitz.com/projects/csscolor/
|
|
*/
|
|
function colourBrightness(string $hex, float $percent) : string {
|
|
// Work out if hash given
|
|
$hash = '';
|
|
|
|
if (stristr($hex, '#')) {
|
|
$hex = str_replace('#', '', $hex);
|
|
$hash = '#';
|
|
}
|
|
|
|
// / HEX TO RGB
|
|
$rgb = [hexdec(substr($hex, 0, 2)), hexdec(substr($hex, 2, 2)), hexdec(substr($hex, 4, 2))];
|
|
|
|
// // CALCULATE
|
|
if (abs($percent) > 1) {
|
|
$percent = $percent / 100;
|
|
}
|
|
|
|
$percent = max(-1.0, min(1.0, $percent));
|
|
|
|
for ($i = 0; $i < 3; $i++) { // See if brighter or darker
|
|
if ($percent > 0) {
|
|
// Lighter
|
|
$rgb[$i] = round($rgb[$i] * $percent) + round(255 * (1 - $percent));
|
|
} else {
|
|
// Darker
|
|
$positivePercent = abs($percent);
|
|
$rgb[$i] = round($rgb[$i] * (1 - $positivePercent));
|
|
}
|
|
|
|
if ($rgb[$i] < 0) {
|
|
$rgb[$i] = 0;
|
|
}
|
|
|
|
// In case rounding up causes us to go to 256
|
|
if ($rgb[$i] > 255) {
|
|
$rgb[$i] = 255;
|
|
}
|
|
}
|
|
|
|
// // RBG to Hex
|
|
$hex = '';
|
|
|
|
for ($i = 0; $i < 3; $i++) {
|
|
// Convert the decimal digit to hex
|
|
$hexDigit = dechex((int) $rgb[$i]);
|
|
|
|
// Add a leading zero if necessary
|
|
if (strlen($hexDigit) == 1) {
|
|
$hexDigit = '0' . $hexDigit;
|
|
}
|
|
|
|
// Append to the hex string
|
|
$hex .= $hexDigit;
|
|
}
|
|
|
|
return $hash . $hex;
|
|
}
|
|
|
|
/**
|
|
* add_unknown_data - Add unknown data area fills to graphs if selected by the admin
|
|
*
|
|
* @param array $graph_array The graph_array data containing all rrdtool graph options
|
|
* @param array $xport_meta If the graph unknown data is true, set that in the
|
|
* xport meta output.
|
|
*
|
|
* @return array The graph_array modified to include unknown data ranges
|
|
*/
|
|
function add_unknown_data(array $graph_array, array &$xport_meta) : array {
|
|
// add the cdef for unknown data
|
|
if (read_config_option('graph_unknown_data') == 'on') {
|
|
if (isset($xport_meta['ignoreItems'])) {
|
|
$xport_meta['ignoreItems'] += 1;
|
|
} else {
|
|
$xport_meta['ignoreItems'] = 1;
|
|
}
|
|
|
|
if (cacti_sizeof($graph_array['defs'])) {
|
|
$tmp_def = '';
|
|
$pluscnt = cacti_sizeof($graph_array['defs']) - 1;
|
|
|
|
foreach ($graph_array['defs'] as $d) {
|
|
$tmp_def .= ($tmp_def != '' ? ',' : '') . "$d,UN";
|
|
}
|
|
|
|
$graph_array['graph_defs'] .= "CDEF:unknowndata='$tmp_def" . str_repeat(',+', $pluscnt);
|
|
$graph_array['graph_defs'] .= ",INF,UNKN,IF'" . RRD_NL;
|
|
|
|
$ud_color = intval(read_config_option('graph_unknown_color'));
|
|
|
|
if ($ud_color > 0) {
|
|
$ud_color = get_color($ud_color);
|
|
} else {
|
|
$ud_color = '8F9286';
|
|
}
|
|
|
|
$ud_opacity = read_config_option('graph_unknown_opacity');
|
|
|
|
if ($ud_opacity == '') {
|
|
$ud_opacity = '7F';
|
|
}
|
|
|
|
$ucolor = $ud_color . $ud_opacity;
|
|
|
|
$graph_array['graph_defs'] .= "AREA:unknowndata#$ucolor:" . RRD_NL;
|
|
}
|
|
}
|
|
|
|
return $graph_array;
|
|
}
|
|
|
|
/**
|
|
* add_business_hours - Add business hours highlight support for all rrdtool based charts
|
|
*
|
|
* @param array $data The graph_array data containing all rrdtool graph options
|
|
* @param mixed $xport_meta
|
|
*
|
|
* @return array The graph_array containing AREA definitions for the business hours
|
|
*
|
|
*/
|
|
function add_business_hours(array $data, mixed &$xport_meta) : array {
|
|
if (read_config_option('business_hours_enable') == 'on' && gnrv('business_hours') == 'true') {
|
|
if ($data['start'] < 0) {
|
|
$bh_graph_start = time() + $data['start'];
|
|
$bh_graph_end = time() + $data['end'];
|
|
} else {
|
|
$bh_graph_start = $data['start'];
|
|
$bh_graph_end = $data['end'];
|
|
}
|
|
|
|
preg_match('/(\d+)\:(\d+)/',read_config_option('business_hours_start'), $bh_start_matches);
|
|
preg_match('/(\d+)\:(\d+)/',read_config_option('business_hours_end'), $bh_end_matches);
|
|
|
|
// Convert all the responses to integers
|
|
$bh_start_matches = array_map('intval', $bh_start_matches);
|
|
$bh_end_matches = array_map('intval', $bh_end_matches);
|
|
|
|
$bh_start_matches[1] = $bh_start_matches[1] ?? 0;
|
|
$bh_start_matches[2] = $bh_start_matches[2] ?? 0;
|
|
$bh_end_matches[1] = $bh_end_matches[1] ?? 0;
|
|
$bh_end_matches[2] = $bh_end_matches[2] ?? 0;
|
|
|
|
$start_bh_time = mktime($bh_start_matches[1], $bh_start_matches[2], 0, intval(date('m', $bh_graph_start)), intval(date('d', $bh_graph_start)), intval(date('Y', $bh_graph_start)));
|
|
$end_bh_time = mktime($bh_end_matches[1], $bh_end_matches[2], 0, intval(date('m', $bh_graph_end)), intval(date('d', $bh_graph_end)), intval(date('Y', $bh_graph_end)));
|
|
|
|
if ($start_bh_time === false || $end_bh_time === false) {
|
|
return $data;
|
|
}
|
|
|
|
if ($start_bh_time < $bh_graph_start) {
|
|
if ($start_bh_time < $end_bh_time) {
|
|
$start_bh_time = $bh_graph_start;
|
|
} else {
|
|
$next_day_time = mktime($bh_start_matches[1], $bh_start_matches[2], 0, intval(date('m', $bh_graph_start)), intval(date('d', $bh_graph_start)) + 1, intval(date('Y', $bh_graph_start)));
|
|
|
|
if ($next_day_time !== false) {
|
|
$start_bh_time = $next_day_time;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the number of days:
|
|
$datediff = $bh_graph_end - $bh_graph_start;
|
|
$num_of_days = round($datediff / (60 * 60 * 24)) + 1;
|
|
|
|
if ($num_of_days <= read_config_option('business_hours_max_days')) {
|
|
for ($day = 0; $day < $num_of_days; $day++) {
|
|
$current_start_bh_time = mktime($bh_start_matches[1], $bh_start_matches[2], 0, intval(date('m', $start_bh_time)), intval(date('d', $start_bh_time)) + $day, intval(date('Y', $start_bh_time)));
|
|
$current_end_bh_time = mktime($bh_end_matches[1], $bh_end_matches[2], 0, intval(date('m', $start_bh_time)), intval(date('d', $start_bh_time)) + $day, intval(date('Y', $start_bh_time)));
|
|
|
|
if ($current_start_bh_time === false || $current_end_bh_time === false) {
|
|
continue;
|
|
}
|
|
|
|
if ($current_start_bh_time < $bh_graph_start) {
|
|
$current_start_bh_time = $bh_graph_start;
|
|
}
|
|
|
|
if ($current_end_bh_time > $bh_graph_end) {
|
|
$current_end_bh_time = $bh_graph_end;
|
|
}
|
|
|
|
$data['graph_defs'] .= 'CDEF:officehours' . $day . '=a,POP,TIME,' . $current_start_bh_time . ',LT,1,0,IF,TIME,' . $current_end_bh_time . ',GT,1,0,IF,MAX,0,GT,0,1,IF' . RRD_NL;
|
|
$data['graph_defs'] .= 'CDEF:dslimit' . $day . '=INF,officehours' . $day . ',*' . RRD_NL;
|
|
|
|
$bh_color = intval(read_config_option('business_hours_color'));
|
|
|
|
if (empty($bh_color)) {
|
|
$bh_color = 'CCCCCC';
|
|
} else {
|
|
$bh_color = get_color($bh_color);
|
|
|
|
if (empty($bh_color)) {
|
|
$bh_color = 'CCCCCC';
|
|
}
|
|
}
|
|
|
|
$bh_opacity = read_config_option('business_hours_opacity');
|
|
|
|
if (empty($bh_opacity)) {
|
|
$bh_opacity = '7F';
|
|
}
|
|
|
|
if (preg_match('/[0-9A-Fa-f]{2}/', $bh_opacity)) {
|
|
$bh_color .= $bh_opacity;
|
|
} else {
|
|
$bh_color .= '7F';
|
|
}
|
|
|
|
if (date('N', $current_start_bh_time) < 6) {
|
|
$data['graph_defs'] .= 'AREA:dslimit' . $day . '#' . $bh_color . RRD_NL;
|
|
}
|
|
|
|
if (date('N', $current_start_bh_time) > 5 && read_config_option('business_hours_hide_weekends') == '') {
|
|
$data['graph_defs'] .= 'AREA:dslimit' . $day . '#' . $bh_color . RRD_NL;
|
|
}
|
|
|
|
if (isset($xport_meta['ignoreItems'])) {
|
|
$xport_meta['ignoreItems'] += 1;
|
|
} else {
|
|
$xport_meta['ignoreItems'] = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|