Files
cacti/lib/dsstats.php
TheWitnessandcopilot-swe-agent 2d516ca0a9 security: consolidate additional verified GHSA fixes for develop (batch 2) (#8065)
* security: contain data source paths within the RRA directory (GHSA-9qff-xccr-rrc5)

* security: escape executable path in exec_background on non-win32 (GHSA-cv7j-p6fh-j4q7)

* security: stop disabled accounts authenticating via remember-me cookie and revoke on disable (GHSA-pqrp-v752-mc4f)

* security: prefer base_url over client Host for the force_https redirect (GHSA-3466-fp7g-c9rf)

* changelog: add fragments for GHSA-9qff-xccr-rrc5, GHSA-cv7j-p6fh-j4q7, GHSA-pqrp-v752-mc4f, GHSA-3466-fp7g-c9rf

* security: address review findings on GHSA batch 2 (develop)

- lib/dsstats.php, poller_maintenance.php: route through
  get_data_source_path() instead of expanding data_source_path directly,
  so the RRA containment check applies to these consumers too (GHSA-9qff)
- lib/functions.php: data_source_path_within_rra() now also validates
  the realpath of each existing ancestor segment, closing a symlink
  pivot that the purely lexical check missed (GHSA-9qff)
- include/global.php: fail closed instead of falling back to SERVER_NAME
  when base_url is unset/invalid, removing the residual open-redirect
  path (GHSA-3466)
- lib/html_utility.php: cacti_force_https_host()/
  cacti_build_https_redirect_url() now parse a scheme-less base_url
  (matching lib/reports.php's own normalization) and preserve a
  configured non-default port instead of silently dropping it (GHSA-3466)
- tests: move DataSourcePathContainmentTest.php into
  tests/Unit/Security/PathTraversal/, and switch its fixtures from a
  fictional CACTI_PATH_RRA path to a real temp directory so the new
  realpath()-based checks (including a symlink-pivot regression case)
  can actually run

* test: fix CACTI_PATH_RRA constant collision in DataSourcePathContainmentTest

CI showed 2 failures: RrdProxyProtocolTest.php runs earlier in the same
Pest process and already defines CACTI_PATH_RRA to a fictional,
non-existent '/var/lib/cacti/rra', so our own definition guard never
ran and realpath(CACTI_PATH_RRA) returned false for every assertion,
failing the two tests that expect containment to be true.

Now ensure the directory backing CACTI_PATH_RRA actually exists on
disk regardless of who defined the constant first, and gracefully
skip only the fixture-dependent assertions if this process can't
create it (e.g. no permission on a system path).

* test: refresh sink inventory baseline for exec_background rename

5fbe90fb4 renamed \ to \ in lib/poller.php's
exec() calls (GHSA-cv7j-p6fh-j4q7) but never regenerated the tracked
sink inventory, so the 'sink and architectural hotspot guard' CI check
has been failing on this branch since that commit. Regenerated via
tests/security/build_sink_inventory.sh | tr -d '\r' | LC_ALL=C sort;
diff confirms only those two lines changed.

* fix: address CI failures on GHSA batch 2 (develop)

- lib/auth.php: verify the remember-me token before revoking a disabled
  account's cache row, so a forged token no longer triggers a database
  write (fixes AuthSystemRegressionIntegrationTest)
- apply php-cs-fixer coding standard fixes (comment style, phpdoc
  alignment, list-to-short-array syntax) flagged by the locked
  integration check
- refresh the architectural hotspots baseline for the exec_background
  safe_filename rename

* fix: update PHPStan baseline for reordered check_auth_cookie and get_data_source_path

- lib/auth.php: reordering the disabled-account check after the token
  match moved one 'enabled' offset access to a new line, bumping the
  file's expected occurrence count from 3 to 4
- lib/functions.php: the GHSA-9qff-xccr-rrc5 containment log line reads
  \['data_source_path'], a genuinely new array|bool access
  not previously tracked

---------

Co-authored-by: copilot-swe-agent <copilot-swe-agent[bot]@users.noreply.github.com>
2026-09-24 10:06:01 -04:00

1582 lines
50 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/ |
+-------------------------------------------------------------------------+
*/
/**
* get_rrdfile_names - this routine returns all of the RRDfiles know to Cacti
* so as to be processed when performing the Daily, Weekly, Monthly and Yearly
* average and peak calculations.
*
* @param int $thread_id The thread to process
* @param int $max_threads The maximum number of threads
*
* @return array The RRDfile names
*/
function get_rrdfile_names(int $thread_id = 1, int $max_threads = 1) : array {
static $dsrows = [];
if ($max_threads == 1) {
return db_fetch_assoc('SELECT dtd.local_data_id, data_source_path, rrd_num AS dsses, GROUP_CONCAT(DISTINCT data_source_name) AS dsnames
FROM data_template_data AS dtd
INNER JOIN data_template_rrd AS dtr
ON dtd.local_data_id = dtr.local_data_id
LEFT JOIN poller_item AS pi
ON pi.local_data_id = dtd.local_data_id
WHERE pi.local_data_id IS NOT NULL
AND data_source_path != ""
AND dtd.local_data_id != 0
GROUP BY dtd.local_data_id');
}
if (cacti_sizeof($dsrows)) {
return $dsrows;
} else {
$dsses_total = db_fetch_cell('SELECT COUNT(DISTINCT dtd.local_data_id)
FROM data_template_data AS dtd
LEFT JOIN poller_item AS pi
ON pi.local_data_id = dtd.local_data_id
WHERE pi.local_data_id IS NOT NULL
AND data_source_path != ""
AND dtd.local_data_id != 0');
$split_size = ceil($dsses_total / $max_threads);
$start = (($thread_id - 1) * $split_size) + 1;
$rows = db_fetch_assoc("SELECT dtd.local_data_id, data_source_path,
rrd_num AS dsses, GROUP_CONCAT(DISTINCT data_source_name) AS dsnames
FROM data_template_data AS dtd
INNER JOIN data_template_rrd AS dtr
ON dtd.local_data_id = dtr.local_data_id
LEFT JOIN poller_item AS pi
ON pi.local_data_id = dtd.local_data_id
WHERE pi.local_data_id IS NOT NULL
AND data_source_path != ''
AND dtd.local_data_id != 0
GROUP BY dtd.local_data_id
LIMIT $start, $split_size");
$dsrows = $rows;
if (cacti_sizeof($rows)) {
return $rows;
} else {
return [];
}
}
}
/**
* dsstats_debug - this simple routine prints a standard message to the console
* when running in debug mode.
*
* @param string $message The message to display
*
* @return void
*/
function dsstats_debug($message) : void {
global $debug;
if ($debug) {
print 'DSSTATS: ' . $message . PHP_EOL;
}
}
/**
* dsstats_get_and_store_ds_avgpeak_values - this routine is a generic routine that takes an time interval as an
* input parameter and then, though additional function calls, reads the RRDfiles for the correct information
* and stores that information into the various database tables.
*
* @param string $interval Either 'daily', 'weekly', 'monthly', or 'yearly'
* @param string $type The statistics type to store
* @param int $thread_id The dsstats parallel thread id
*
* @return void
*/
function dsstats_get_and_store_ds_avgpeak_values(string $interval, string $type, int $thread_id = 1) : void {
global $total_user, $total_system, $total_real, $total_dsses;
global $user_time, $system_time, $real_time, $rrd_files;
$user_time = 0;
$system_time = 0;
$real_time = 0;
$dsses = 0;
dsstats_debug(sprintf('Processing %s for Thread %s', $interval, $thread_id));
$max_threads = read_config_option('dsstats_parallel');
$mode = read_config_option('dsstats_mode');
$peak = read_config_option('dsstats_peak') == 'on' ? true : false;
if (empty($max_threads)) {
$max_threads = 1;
set_config_option('dsstats_parallel', '1');
}
if (empty($thread_id)) {
$thread_id = 1;
$max_threads = 1;
}
$rrdfiles = get_rrdfile_names($thread_id, $max_threads);
$stats = [];
$rrd_files += cacti_sizeof($rrdfiles);
$use_proxy = (read_config_option('storage_location') > 0 ? true : false);
// open a pipe to rrdtool for writing and reading
if ($use_proxy) {
$rrd_process = rrd_init(false);
} else {
$rrd_process = dsstats_rrdtool_init();
// dsstats_rrdtool_init() returns [false, false] when RRDtool could not be
// spawned; without live pipes the execute path would fwrite() to null and
// crash the poller, so bail out of this run gracefully
if (!is_resource($rrd_process[0])) {
cacti_log('ERROR: DSStats unable to obtain RRDtool process, skipping run', false, 'DSSTATS');
return;
}
}
if (cacti_sizeof($rrdfiles)) {
foreach ($rrdfiles as $file) {
$dsses += $file['dsses'];
if ($file['data_source_path'] != '') {
// resolve through get_data_source_path() so the RRA containment check applies here too
$rrdfile = get_data_source_path($file['local_data_id'], true);
$local_data_id = $file['local_data_id'];
$dsnames = explode(',', $file['dsnames']);
$stats[$local_data_id] = dsstats_obtain_data_source_avgpeak_values($local_data_id, $rrdfile, $interval, $mode, $peak, $rrd_process);
} else {
$data_source_name = db_fetch_cell_prepared('SELECT name_cache
FROM data_template_data
WHERE local_data_id = ?',
[$file['local_data_id']]);
cacti_log("WARNING: Data Source '$data_source_name' is damaged and contains no path. Please delete and re-create both the Graph and Data Source.", false, 'DSSTATS');
}
}
}
if ($use_proxy) {
rrd_close($rrd_process);
} else {
dsstats_rrdtool_close($rrd_process);
}
dsstats_write_buffer($stats, $interval, $mode);
if (!empty($type)) {
$total_user += $user_time;
$total_system += $system_time;
$total_real += $real_time;
$total_dsses += $dsses;
set_config_option('dsstats_rrd_system_' . $type . '_' . $thread_id, $total_system);
set_config_option('dsstats_rrd_user_' . $type . '_' . $thread_id, $total_user);
set_config_option('dsstats_rrd_real_' . $type . '_' . $thread_id, $total_real);
set_config_option('dsstats_total_rrds_' . $type . '_' . $thread_id, $rrd_files);
set_config_option('dsstats_total_dsses_' . $type . '_' . $thread_id, $total_dsses);
}
}
/**
* dsstats_write_buffer - this routine provide bulk database insert services to the various tables that store
* the average and peak information for Data Sources.
*
* @param array $stats_array A multi dimensional array keyed by the local_data_id that contains both
* the average and max values for each internal RRDfile Data Source.
* @param string $interval 'daily', 'weekly', 'monthly', and 'yearly'. Used for determining the table to
* update during the dumping of the buffer.
* @param int $mode The mode of collection legacy '0' or advanced '1'
*
* @return void
*/
function dsstats_write_buffer(array &$stats_array, string $interval, int $mode) : void {
$outbuf = '';
$out_length = 0;
$i = 1;
$max_packet = 256000;
// Format $stats[ldi][avg|max][rrd_name][metric] = $value
if ($mode == 1) {
$sql_prefix = "INSERT INTO data_source_stats_$interval
(local_data_id, rrd_name, cf, average, peak, p95n, p90n, p75n, p50n, p25n, sum, stddev, lslslope, lslint, lslcorrel) VALUES";
$sql_suffix = ' ON DUPLICATE KEY UPDATE
average=VALUES(average),
peak=VALUES(peak),
p95n=VALUES(p95n),
p90n=VALUES(p90n),
p75n=VALUES(p75n),
p50n=VALUES(p50n),
p25n=VALUES(p25n),
sum=VALUES(sum),
stddev=VALUES(stddev),
lslslope=VALUES(lslslope),
lslint=VALUES(lslint),
lslcorrel=VALUES(lslcorrel)';
} else {
$sql_prefix = "INSERT INTO data_source_stats_$interval (local_data_id, rrd_name, cf, average, peak) VALUES";
$sql_suffix = ' ON DUPLICATE KEY UPDATE
average=VALUES(average),
peak=VALUES(peak)';
}
$overhead = strlen($sql_prefix) + strlen($sql_suffix);
// don't attempt to process an empty array
// Format $stats[ldi][avg|max][rrd_name][metric] = $value
if (cacti_sizeof($stats_array)) {
foreach ($stats_array as $local_data_id => $ldi_stats) {
if (cacti_sizeof($ldi_stats)) {
foreach ($ldi_stats as $cf => $cf_stats) {
if ($cf == 'avg') {
$mycf = '0';
} else {
$mycf = '1';
}
foreach ($cf_stats as $rrd_name => $stats) {
if ($mode == 0) {
$outbuf .= ($i == 1 ? ' ' : ', ') . "('" .
$local_data_id . "','" .
$rrd_name . "','" .
$mycf . "','" .
$stats['avg'] . "','" .
$stats['peak'] . "')";
} else {
$outbuf .= ($i == 1 ? ' ' : ', ') . "('" .
$local_data_id . "','" .
$rrd_name . "','" .
$mycf . "','" .
$stats['avg'] . "','" .
$stats['peak'] . "','" .
$stats['p95n'] . "','" .
$stats['p90n'] . "','" .
$stats['p75n'] . "','" .
$stats['p50n'] . "','" .
$stats['p25n'] . "','" .
$stats['sum'] . "','" .
$stats['stddev'] . "','" .
$stats['lslslope'] . "','" .
$stats['lslint'] . "','" .
$stats['lslcorrel'] . "')";
}
$out_length += strlen($outbuf);
if (($out_length + $overhead) > $max_packet) {
db_execute($sql_prefix . $outbuf . $sql_suffix);
$outbuf = '';
$out_length = 0;
$i = 1;
} else {
$i++;
}
}
}
} else {
$host_id = db_fetch_cell_prepared('SELECT host_id
FROM data_local
WHERE id = ?',
[$local_data_id]);
cacti_log(sprintf('WARNING: Problem with Data Source for Device[%s], DS[%s], Interval[%s]', $host_id, $local_data_id, ucfirst($interval)), false, 'DSSTATS');
}
}
// flush the buffer if it still has elements in it
if ($out_length > 0) {
db_execute($sql_prefix . $outbuf . $sql_suffix);
}
}
}
/**
* dsstats_obtain_data_source_avgpeak_values - this routine, given the rrdfile name, interval and RRDtool process
* pipes, will obtain the average a peak values from the RRDfile. It does this in two steps:
*
* 1) It first reads the RRDfile's information header to obtain all of the internal data source names,
* poller interval and consolidation functions.
* 2) Based upon the available consolidation functions, it then grabs either AVERAGE, and MAX, or just AVERAGE
* in the case where the MAX consolidation function is not included in the RRDfile, and then proceeds to
* gather data from the RRDfile for the time period in question. It allows RRDtool to select the RRA to
* use by simply limiting the number of rows to be returned to the default.
*
* Once it has all of the information from the RRDfile. It then decomposes the resulting XML file to its
* components and then calculates the AVERAGE and MAX values from that data and returns an array to the calling
* function for storage into the respective database table.
*
* @param int $local_data_id The Cacti Local Data Id
* @param string $rrdfile The rrdfile to process
* @param string $interval The interval type to process
* @param int $mode If we should be collecting legacy stats or not
* @param bool $peak If the peak should be take from the max cf
* @param array $rrd_process Pipes to the background RRDtool process
*
* @return array An array of AVERAGE, and MAX values in an RRDfile by Data Source name
*/
function dsstats_obtain_data_source_avgpeak_values(int $local_data_id, string $rrdfile, string $interval, int $mode, bool $peak, array $rrd_process) : array {
global $user_time, $system_time, $real_time;
$use_proxy = (read_config_option('storage_location') ? true : false);
if ($use_proxy) {
$file_exists = rrdtool_execute('file_exists ' . cacti_escapeshellarg($rrdfile), true, RRDTOOL_OUTPUT_BOOLEAN, $rrd_process, 'DSSTATS');
} else {
clearstatcache();
$file_exists = file_exists($rrdfile);
}
$dsvalues = [];
// don't attempt to get information if the file does not exist
if ($file_exists) {
$stats_command = db_fetch_cell_prepared('SELECT stats_command
FROM data_source_stats_command_cache
WHERE local_data_id = ?',
[$local_data_id]);
if ($stats_command == '') {
$stats_command = dsstats_get_stats_command($local_data_id, $rrdfile, $use_proxy, $mode, $peak, $rrd_process);
}
if ($stats_command !== false) {
// change the interval to something RRDtool understands
switch($interval) {
case 'daily':
$interval = 'day';
break;
case 'weekly':
$interval = 'week';
break;
case 'monthly':
$interval = 'month';
break;
case 'yearly':
$interval = 'year';
break;
}
// now execute the graph command
$stats_cmd = 'graph x --start now-1' . $interval . ' --end now ' . trim($stats_command);
// print $stats_cmd . PHP_EOL . PHP_EOL;
if ($use_proxy) {
$xport_data = rrdtool_execute($stats_cmd, false, RRDTOOL_OUTPUT_STDOUT, $rrd_process, 'DSSTATS');
} else {
$xport_data = dsstats_rrdtool_execute($stats_cmd, $rrd_process);
}
// process the xport array and return average and peak values
if ($xport_data != '') {
$xport_array = explode("\n", $xport_data);
if (cacti_sizeof($xport_array)) {
foreach ($xport_array as $index => $line) {
$line = trim($line);
if ($line == '' || $line == '0x0') {
continue;
}
if ($index > 0) {
// Catch the last line
if (str_starts_with($line, 'OK')) {
$line = trim($line, ' OK');
$parts = explode(' ', $line);
// print $line . PHP_EOL;
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;
}
}
break;
} else {
// Get the rrd_name and the remainder
$parts = explode('-', $line);
$rrd_name = $parts[0];
$namvalpt = $parts[1];
// Get the combined variable and value
$parts = explode('=', $namvalpt);
$variable = $parts[0];
$value = $parts[1];
// Get the metric and the mode
$parts = explode('_', $variable);
$metric = $parts[0];
$amode = $parts[1];
if (stripos($value, 'nan') === false) {
$dsvalues[$amode][$rrd_name][$metric] = $value;
} else {
$dsvalues[$amode][$rrd_name][$metric] = 'NULL';
}
}
}
}
return $dsvalues;
}
}
}
} elseif (($interval == 'daily') || ($interval == 'day')) {
// only alarm if performing the 'daily' averages
cacti_log("WARNING: File does not exist! DS[$local_data_id], FILE[" . $rrdfile . ']', false, 'DSSTATS');
}
return $dsvalues;
}
function dsstats_get_stats_command(int $local_data_id, string $rrdfile, bool $use_proxy, int $mode, bool $peak, array $rrd_process) : mixed {
global $user_time, $system_time, $real_time;
// high speed or snail speed
if ($use_proxy) {
$info = rrdtool_execute('info ' . cacti_escapeshellarg($rrdfile), false, RRDTOOL_OUTPUT_STDOUT, $rrd_process, 'DSSTATS');
} else {
$info = dsstats_rrdtool_execute('info ' . cacti_escapeshellarg($rrdfile), $rrd_process);
}
/**
* Get the current values of stats and peak settings to see if there is any change to purge the cache
*/
$temp_mode = read_config_option('dsstats_mode');
$temp_peak = read_config_option('dsstats_peak');
set_config_option('dsstats_temp_mode', $temp_mode);
set_config_option('dsstats_temp_peak', $temp_peak);
$length = strlen($info);
$command = '';
// don't do anything if RRDfile did not return data
if ($info != '') {
$info_array = explode("\n", $info);
$average = false;
$max = false;
$dsnames = [];
/* figure out what is in this RRDfile. Assume CF Uniformity as Cacti does not allow async rrdfiles.
* also verify the consolidation functions in the RRDfile for average and max calculations.
*/
if (cacti_sizeof($info_array)) {
foreach ($info_array as $line) {
if (str_contains($line, 'ds[')) {
$parts = explode(']', $line);
$parts2 = explode('[', $parts[0]);
$dsnames[trim($parts2[1])] = 1;
} elseif (str_contains($line, '.cf')) {
$parts = explode('=', $line);
if (str_contains($parts[1], 'AVERAGE')) {
$average = true;
}
if (str_contains($parts[1], 'MAX')) {
$max = true;
}
} elseif (str_contains($line, 'step')) {
$parts = explode('=', $line);
$poller_interval = trim($parts[1]);
}
}
}
// create the command syntax to get data
// assume that an RRDfile has not more than 62 data sources
$defs = 'abcdefghijklmnopqrstuvwxyz012345789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$i = 0;
$j = 0;
$def = '';
$command = '';
$dsvalues = [];
// escape the file name if on Windows
if (CACTI_SERVER_OS != 'unix') {
$rrdfile = str_replace(':', '\\:', $rrdfile);
}
// setup the graph command by parsing through the internal data source names
if (cacti_sizeof($dsnames)) {
foreach ($dsnames as $dsname => $present) {
$mydata_avg = $defs[$j] . $defs[$i] . '_a';
$mydata_max = $defs[$j] . $defs[$i] . '_m';
if ($average) {
$def .= 'DEF:' . $mydata_avg . '="' . $rrdfile . '":' . $dsname . ':AVERAGE ';
$command .= " VDEF:{$mydata_avg}_aa=$mydata_avg,AVERAGE PRINT:{$mydata_avg}_aa:{$dsname}-avg_avg=%lf";
$command .= " VDEF:{$mydata_avg}_am=$mydata_avg,MAXIMUM PRINT:{$mydata_avg}_am:{$dsname}-peak_avg=%lf";
$i++;
}
if ($max && $peak) {
$def .= 'DEF:' . $mydata_max . '="' . $rrdfile . '":' . $dsname . ':MAX ';
$command .= " VDEF:{$mydata_max}_ma=$mydata_max,AVERAGE PRINT:{$mydata_max}_ma:{$dsname}-avg_max=%lf";
$command .= " VDEF:{$mydata_max}_mm=$mydata_max,MAXIMUM PRINT:{$mydata_max}_mm:{$dsname}-peak_max=%lf";
$i++;
}
if ($mode == 1) {
$pt = ['95', '90', '75', '50', '25'];
if ($average) {
foreach ($pt as $s) {
$command .= " VDEF:{$mydata_avg}_p{$s}n={$mydata_avg},$s,PERCENTNAN PRINT:{$mydata_avg}_p{$s}n:{$dsname}-p{$s}n_avg=%lf";
}
// TOTAL
$command .= " VDEF:{$mydata_avg}_sum=$mydata_avg,TOTAL PRINT:{$mydata_avg}_sum:{$dsname}-sum_avg=%lf";
// STDDEV
$command .= " VDEF:{$mydata_avg}_stddev=$mydata_avg,STDEV PRINT:{$mydata_avg}_stddev:{$dsname}-stddev_avg=%lf";
// LSLSLOPE
$command .= " VDEF:{$mydata_avg}_lslslope=$mydata_avg,LSLSLOPE PRINT:{$mydata_avg}_lslslope:{$dsname}-lslslope_avg=%lf";
// LSLINT
$command .= " VDEF:{$mydata_avg}_lslint=$mydata_avg,LSLINT PRINT:{$mydata_avg}_lslint:{$dsname}-lslint_avg=%lf";
// LSLCORREL
$command .= " VDEF:{$mydata_avg}_lslcorrel=$mydata_avg,LSLCORREL PRINT:{$mydata_avg}_lslcorrel:{$dsname}-lslcorrel_avg=%lf";
}
if ($max && $peak) {
foreach ($pt as $s) {
$command .= " VDEF:{$mydata_max}_p{$s}n={$mydata_max},$s,PERCENTNAN PRINT:{$mydata_max}_p{$s}n:{$dsname}-p{$s}n_max=%lf";
}
// TOTAL
$command .= " VDEF:{$mydata_max}_sum=$mydata_max,TOTAL PRINT:{$mydata_max}_sum:{$dsname}-sum_max=%lf";
// STDDEV
$command .= " VDEF:{$mydata_max}_stddev=$mydata_max,STDEV PRINT:{$mydata_max}_stddev:{$dsname}-stddev_max=%lf";
// LSLSLOPE
$command .= " VDEF:{$mydata_max}_lslslope=$mydata_max,LSLSLOPE PRINT:{$mydata_max}_lslslope:{$dsname}-lslslope_max=%lf";
// LSLINT
$command .= " VDEF:{$mydata_max}_lslint=$mydata_max,LSLINT PRINT:{$mydata_max}_lslint:{$dsname}-lslint_max=%lf";
// LSLCORREL
$command .= " VDEF:{$mydata_max}_lslcorrel=$mydata_max,LSLCORREL PRINT:{$mydata_max}_lslcorrel:{$dsname}-lslcorrel_max=%lf";
}
}
if ($i > 50) {
$j++;
$i = 0;
}
}
}
$command = trim($def) . ' ' . $command;
db_execute_prepared('REPLACE INTO data_source_stats_command_cache
(local_data_id, stats_command)
VALUES(?, ?)',
[$local_data_id, $command]);
return $command;
}
return false;
}
/**
* dsstats_log_statistics - provides generic timing message to both the Cacti log and the settings
* table so that the statistics can be graphed as well.
*
* @param string $type The type of statistics to log, either 'HOURLY', 'DAILY', 'BOOST' or 'MAJOR'.
*
* @return void
*/
function dsstats_log_statistics(string $type) : void {
global $start;
dsstats_debug($type);
$sub_type = '';
if ($type == 'HOURLY') {
$sub_type = '';
} elseif ($type == 'MAJOR') {
$sub_type = 'dchild';
} elseif ($type == 'DAILY') {
$sub_type = 'child';
} elseif ($type == 'BOOST') {
$sub_type = 'bchild';
}
// take time and log performance data
$end = microtime(true);
if ($sub_type != '') {
$totals = dsstats_fetch_statistics_totals('%' . $sub_type . '%');
$rrd_user = $totals['rrd_user'];
$rrd_system = $totals['rrd_system'];
$rrd_real = $totals['rrd_real'];
$rrd_files = $totals['rrd_files'];
$dsses = $totals['dsses'];
$processes = read_config_option('dsstats_parallel');
$cacti_stats = sprintf('Time:%01.2f Type:%s Threads:%s RRDfiles:%s DSSes:%s RRDUser:%01.2f RRDSystem:%01.2f RRDReal:%01.2f', $end - $start, $type, $processes, $rrd_files, $dsses, $rrd_user, $rrd_system, $rrd_real);
db_execute("DELETE FROM settings
WHERE name LIKE 'dsstats_rrd_%$sub_type%'
OR name LIKE 'dsstats_total_rrds%_$sub_type%'
OR name LIKE 'dsstats_total_dsses%_$sub_type%'");
} else {
$cacti_stats = sprintf('Time:%01.2f Type:%s', $end - $start, $type);
}
// take time and log performance data
$start = microtime(true);
// log to the database
set_config_option('stats_dsstats_' . $type, $cacti_stats);
// log to the logfile
cacti_log('DSSTATS STATS: ' . $cacti_stats , true, 'SYSTEM');
}
/**
* dsstats_log_child_stats - logs dsstats child process information
*
* @param string $type The type of child, MAJOR, DAILY, BOOST
* @param int $thread_id The parallel thread id
* @param float $total_time The total time to collect date
*
* @return void
*/
function dsstats_log_child_stats(string $type, int $thread_id, float $total_time) : void {
$totals = dsstats_fetch_statistics_totals('%' . $type . '_' . $thread_id . '%');
$rrd_user = $totals['rrd_user'];
$rrd_system = $totals['rrd_system'];
$rrd_real = $totals['rrd_real'];
$rrd_files = $totals['rrd_files'];
$dsses = $totals['dsses'];
$cacti_stats = sprintf('Time:%01.2f Type:%s ProcessNumber:%s RRDfiles:%s DSSes:%s RRDUser:%01.2f RRDSystem:%01.2f RRDReal:%01.2f', $total_time, cacti_strtoupper($type), $thread_id, $rrd_files, $dsses, $rrd_user, $rrd_system, $rrd_real);
cacti_log('DSSTATS CHILD STATS: ' . $cacti_stats, true, 'SYSTEM');
}
/**
* Fetch all DSStats timing and count totals in one database round trip.
*
* @param string $suffix_pattern SQL LIKE pattern appended to each metric prefix.
*
* @return array{rrd_user: float, rrd_system: float, rrd_real: float, rrd_files: float, dsses: float}
*/
function dsstats_fetch_statistics_totals(string $suffix_pattern) : array {
$patterns = [
'dsstats_rrd_user_' . $suffix_pattern,
'dsstats_rrd_system_' . $suffix_pattern,
'dsstats_rrd_real_' . $suffix_pattern,
'dsstats_total_rrds_' . $suffix_pattern,
'dsstats_total_dsses_' . $suffix_pattern,
];
$totals = db_fetch_row_prepared('SELECT
COALESCE(SUM(CASE WHEN name LIKE ? THEN value END), 0) AS rrd_user,
COALESCE(SUM(CASE WHEN name LIKE ? THEN value END), 0) AS rrd_system,
COALESCE(SUM(CASE WHEN name LIKE ? THEN value END), 0) AS rrd_real,
COALESCE(SUM(CASE WHEN name LIKE ? THEN value END), 0) AS rrd_files,
COALESCE(SUM(CASE WHEN name LIKE ? THEN value END), 0) AS dsses
FROM settings
WHERE name LIKE \'dsstats_%\'',
$patterns);
return [
'rrd_user' => (float) ($totals['rrd_user'] ?? 0),
'rrd_system' => (float) ($totals['rrd_system'] ?? 0),
'rrd_real' => (float) ($totals['rrd_real'] ?? 0),
'rrd_files' => (float) ($totals['rrd_files'] ?? 0),
'dsses' => (float) ($totals['dsses'] ?? 0),
];
}
/**
* dsstats_error_handler - this routine logs all PHP error transactions
* to make sure they are properly logged.
*
* @param int $errno The errornum reported by the system
* @param string $errmsg The error message provides by the error
* @param string $filename The filename that encountered the error
* @param int $linenum The line number where the error occurred
* @param array $vars The current state of PHP variables.
*
* @return bool always returns true for some reason
*/
function dsstats_error_handler(int $errno, string $errmsg, string $filename, int $linenum, array $vars = []) : bool {
if (read_config_option('log_verbosity') >= POLLER_VERBOSITY_DEBUG) {
// define all error types
$errortype = [
E_ERROR => 'Error',
E_WARNING => 'Warning',
E_PARSE => 'Parsing Error',
E_NOTICE => 'Notice',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User Notice',
E_RECOVERABLE_ERROR => 'Catchable Fatal Error'
];
// create an error string for the log
$err = "ERRNO:'" . $errno . "' TYPE:'" . $errortype[$errno] .
"' MESSAGE:'" . $errmsg . "' IN FILE:'" . $filename .
"' LINE NO:'" . $linenum . "'";
// let's ignore some lesser issues
if (str_contains($errmsg, 'date_default_timezone')) {
return true;
}
if (str_contains($errmsg, 'Only variables')) {
return true;
}
// log the error to the Cacti log
cacti_log('PROGERR: ' . $err, false, 'DSSTATS');
return false;
}
return true;
}
/**
* dsstats_poller_output - this routine runs in parallel with the cacti poller and
* populates the last and cache tables. On larger systems, it should be noted that
* the memory overhead for the global arrays, $ds_types, $ds_last, $ds_multi
* could be serval hundred megabytes. So, this should be kept in mind when running the
* sizing your system.
*
* The routine basically loads those 4 structures into memory, and then uses them to
* determine what should be stored in both the Cache and the Last tables. The 4 structures
* contain the following information:
*
* $ds_types - The type of data source, keyed by the local_data_id and the rrd_name stored inside
* of the RRDfile.
* $ds_last - For the COUNTER, and DERIVE DS types, the last measured and stored value.
* $ds_multi - For Multi Part responses, stores the mapping of the Data Input Fields to the
* Internal RRDfile DS names.
*
* The routine loops through all poller output items and makes decisions relative to the output
* that should be stored into the two tables, and then bulk inserts that information once
* all poller items have been processed.
*
* The purpose for loading then entire structures into memory at one time is to reduce the latency
* related to multiple database calls. The author believed that PHP's array hashing algorithms
* would be as fast, if not faster, than MySQL, when considering the transaction overhead and therefore
* chose this method.
*
* @param mixed $rrd_update_array The output from the poller output table to be processed by dsstats
*
* @return void
*/
function dsstats_poller_output(mixed &$rrd_update_array) : void {
static $ds_types = [];
static $ds_multi = [];
// suppress warnings
if (defined('E_DEPRECATED')) {
error_reporting(E_ALL ^ E_DEPRECATED);
} else {
error_reporting(E_ALL);
}
// install the dsstats error handler
set_error_handler('dsstats_error_handler');
// do not make any calculations unless enabled
if (read_config_option('dsstats_enable') == 'on') {
if (cacti_sizeof($rrd_update_array) > 0) {
$local_data_ids = [];
foreach ($rrd_update_array as $data_source) {
$local_data_id = (int) ($data_source['local_data_id'] ?? 0);
if ($local_data_id > 0) {
$local_data_ids[$local_data_id] = $local_data_id;
}
}
$missing_data_ids = array_values(array_diff($local_data_ids, array_keys($ds_types)));
if ($missing_data_ids !== []) {
$placeholders = implode(',', array_fill(0, count($missing_data_ids), '?'));
$metadata = db_fetch_assoc_prepared("SELECT dtr.local_data_id, dtr.data_source_name,
dtr.data_source_type_id, dtd.rrd_step, dtr.rrd_maximum
FROM data_template_rrd AS dtr
INNER JOIN data_template_data AS dtd
ON dtd.local_data_id = dtr.local_data_id
WHERE dtr.local_data_id IN ($placeholders)",
$missing_data_ids);
foreach ($missing_data_ids as $local_data_id) {
$ds_types[$local_data_id] = [];
}
foreach ($metadata as $row) {
$ds_types[(int) $row['local_data_id']][$row['data_source_name']] = [
'data_source_type_id' => $row['data_source_type_id'],
'rrd_step' => $row['rrd_step'],
'rrd_maximum' => $row['rrd_maximum'],
];
}
}
$last_values = [];
if ($local_data_ids !== []) {
$placeholders = implode(',', array_fill(0, count($local_data_ids), '?'));
$last_rows = db_fetch_assoc_prepared("SELECT local_data_id, rrd_name, `value`
FROM data_source_stats_hourly_last
WHERE local_data_id IN ($placeholders)",
array_values($local_data_ids));
foreach ($last_rows as $row) {
$last_values[(int) $row['local_data_id']][$row['rrd_name']] = $row['value'];
}
}
// we will assume a smaller than the max packet size. This would appear to be around the sweat spot.
$max_packet = '264000';
// initialize some variables related to the DB inserts
$outbuf = '';
$sql_cache_prefix = 'INSERT INTO data_source_stats_hourly_cache (local_data_id, rrd_name, time, `value`) VALUES';
$sql_last_prefix = 'INSERT INTO data_source_stats_hourly_last (local_data_id, rrd_name, `value`, calculated) VALUES';
$sql_suffix = ' ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)';
$sql_last_suffix = ' ON DUPLICATE KEY UPDATE `value`=VALUES(`value`), `calculated`=VALUES(`calculated`)';
$overhead = strlen($sql_cache_prefix) + strlen($sql_suffix);
$overhead_last = strlen($sql_last_prefix) + strlen($sql_last_suffix);
/* make the association between the multi-part name value pairs and the RRDfile internal
* data source names.
*/
if (!cacti_sizeof($ds_multi)) {
$ds_multi = array_rekey(
db_fetch_assoc('SELECT DISTINCT data_name, data_source_name
FROM graph_templates_item AS gti
INNER JOIN data_template_rrd AS dtr
ON gti.task_item_id = dtr.id
INNER JOIN data_input_fields AS dif
ON dif.id = dtr.data_input_field_id
WHERE dtr.data_input_field_id != 0'),
'data_name', 'data_source_name'
);
}
// required for updating tables
$cache_i = 1;
$last_i = 1;
$out_length = 0;
$last_length = 0;
$lastbuf = '';
$cachebuf = '';
// process each array
$n = 1;
foreach ($rrd_update_array as $data_source) {
if (isset($data_source['times'])) {
foreach ($data_source['times'] as $time => $sample) {
foreach ($sample as $ds => $value) {
$result['local_data_id'] = $data_source['local_data_id'];
$result['rrd_name'] = $ds;
$result['time'] = date('Y-m-d H:i:s', $time);
$currentval = '';
if (is_numeric($value)) {
$result['output'] = $value;
} elseif ($value == 'U' || cacti_strtolower($value) == 'nan') {
$result['output'] = 'NULL';
} else {
$result['output'] = 'NULL';
cacti_log('ERROR: Output from local_data_id ' .
$data_source['local_data_id'] .
", for RRDfile DS Name '$ds', is invalid. " .
"It outputs was : '" . $value . "'. " .
'Please check your script or data input method for errors.');
}
$lastval = '';
$ds_metadata = $ds_types[(int) $result['local_data_id']][$result['rrd_name']] ?? [];
$polling_interval = $ds_metadata['rrd_step'] ?? read_config_option('poller_interval');
$ds_type = $ds_metadata['data_source_type_id'] ?? 0;
switch ($ds_type) {
case 2: // COUNTER
case 6: // DCOUNTER
// get the last values from the database for COUNTER and DERIVE data sources
$ds_last = $last_values[(int) $result['local_data_id']][$result['rrd_name']] ?? false;
if ($ds_last == '' || $ds_last == 'NULL') {
$currentval = 'NULL';
} elseif ($result['output'] == 'NULL') {
$currentval = 'NULL';
} elseif ($result['output'] >= $ds_last) {
// everything is normal
$currentval = $result['output'] - $ds_last;
} else {
$max_value = $ds_metadata['rrd_maximum'] ?? 'U';
// possible overflow, see if its 32bit or 64bit
if ($ds_last > 4294967295) {
$currentval = (18446744073709551615 - $ds_last) + $result['output'];
} else {
$currentval = (4294967295 - $ds_last) + $result['output'];
}
if ($max_value != 'U' && $currentval > $max_value) {
$currentval = 'NULL';
}
}
if ($currentval != 'NULL') {
$currentval /= $polling_interval;
if ($ds_type == 6) {
$currentval = round($currentval, 0);
}
}
$lastval = $result['output'];
if ($ds_type == 6 && $lastval != 'NULL') {
$lastval = round($lastval, 0);
}
break;
case 3: // DERIVE
case 7: // DDERIVE
// get the last values from the database for COUNTER and DERIVE data sources
$ds_last = $last_values[(int) $result['local_data_id']][$result['rrd_name']] ?? false;
if ($ds_last == '') {
$currentval = 'NULL';
} elseif ($result['output'] != 'NULL') {
$currentval = ($result['output'] - $ds_last) / $polling_interval;
if ($ds_type == 7) {
$currentval = round($currentval, 0);
}
} else {
$currentval = 'NULL';
}
$lastval = $result['output'];
if ($ds_type == 7 && $lastval != 'NULL') {
$lastval = round($lastval, 0);
}
break;
case 4: // ABSOLUTE
if (!dsstats_is_unknown_data($result['output'])) {
$currentval = abs($result['output']);
$lastval = $currentval;
} else {
$currentval = 'NULL';
$lastval = $currentval;
}
break;
case 1: // GAUGE
if (!dsstats_is_unknown_data($result['output'])) {
$currentval = $result['output'];
$lastval = $result['output'];
} else {
$currentval = 'NULL';
$lastval = $currentval;
}
break;
default:
cacti_log("WARNING: Unknown RRDtool Data Type '$ds_type', For '" . $result['rrd_name'] . "'", false, 'DSSTATS');
break;
}
// when doing bulk inserts, the second record is different
if ($cache_i == 1) {
$cache_delim = ' ';
} else {
$cache_delim = ', ';
}
if ($last_i == 1) {
$last_delim = ' ';
} else {
$last_delim = ', ';
}
if ($currentval == '' || $currentval == '-') {
$currentval = 'NULL';
}
// setup the output buffer for the cache first
$cachebuf .=
$cache_delim . '(' .
(int) $result['local_data_id'] . ', ' .
db_qstr($result['rrd_name']) . ', ' .
db_qstr($result['time']) . ', ' .
$currentval . ')';
$out_length = strlen($cachebuf);
// now do the last value, if applicable
if ($lastval != '') {
$last_values[(int) $result['local_data_id']][$result['rrd_name']] = $lastval;
$lastbuf .=
$last_delim . '(' .
(int) $result['local_data_id'] . ', ' .
db_qstr($result['rrd_name']) . ', ' .
$lastval . ', ' .
$currentval . ')';
$last_i++;
$last_length = strlen($lastbuf);
}
// if we exceed our output buffer, it's time to write
if ((($out_length + $overhead) > $max_packet) ||
(($last_length + $overhead_last) > $max_packet)) {
db_execute($sql_cache_prefix . $cachebuf . $sql_suffix);
if ($last_i > 1) {
db_execute($sql_last_prefix . $lastbuf . $sql_last_suffix);
}
$cachebuf = '';
$lastbuf = '';
$out_length = 0;
$last_length = 0;
$cache_i = 1;
$last_i = 1;
} else {
$cache_i++;
}
$n++;
if (($n % 1000) == 0) {
print '.';
}
}
}
}
}
if ($cache_i > 1) {
db_execute($sql_cache_prefix . $cachebuf . $sql_suffix);
}
if ($last_i > 1) {
db_execute($sql_last_prefix . $lastbuf . $sql_last_suffix);
}
}
}
// restore original error handler
restore_error_handler();
}
/**
* checks the output from the rrdfile column and returns false if it's unknown data
*
* @param string $value - The rrdtool value data
*
* @return bool
*/
function dsstats_is_unknown_data(string $value) : bool {
if ($value != 'NULL' && $value != 'U' && cacti_strtolower($value) != 'nan') {
return false;
} else {
return true;
}
}
/**
* dsstats_boost_bottom - this routine accommodates mass updates after the boost process
* has completed. The use of boost will require boost version 2.5 or above. The idea
* if that daily averages will be updated on the boost cycle.
*
* @return void
*/
function dsstats_boost_bottom() : void {
global $total_user, $total_system, $total_real, $total_dsses;
$total_user = 0;
$total_system = 0;
$total_real = 0;
$total_dsses = 0;
if (read_config_option('dsstats_enable') == 'on') {
include_once(CACTI_PATH_LIBRARY . '/rrd.php');
// run the daily stats. log to database to prevent secondary runs
set_config_option('dsstats_last_daily_run_time', date('Y-m-d G:i:s', time()));
// run the daily stats
$expected_children = dsstats_launch_children('bmaster');
dsstats_wait_for_children('bmaster', $expected_children);
dsstats_get_and_store_ds_avgpeak_values('daily', 'child', 1);
dsstats_log_statistics('DAILY');
}
}
/**
* dsstats_wait_for_children - waits for launched dsstats children of the given
* type to finish. exec_background() is non-blocking, so a bare
* `while (dsstats_processes_running($type))` can observe zero registered
* children before any of them have started, mistaking "not started yet" for
* "already done" and returning immediately -- the same startup race Boost's
* boost_all_children_registered() barrier was added to close (see
* poller_boost.php). This applies the same shape: wait for the running count
* to reach the launched count before trusting it can fall back to zero.
*
* @param string $type The process type (e.g. 'bmaster')
* @param int $expected_children Number of children dsstats_launch_children() launched
*
* @return void
*/
function dsstats_wait_for_children(string $type, int $expected_children) : void {
if ($expected_children <= 0) {
return;
}
$startup_deadline = time() + 30;
while (dsstats_processes_running($type) < $expected_children && time() < $startup_deadline) {
sleep(1);
}
if (dsstats_processes_running($type) < $expected_children) {
cacti_log(sprintf('WARNING: DSStats startup barrier timed out; %d of %d %s children registered before draining.', dsstats_processes_running($type), $expected_children, $type), false, 'DSSTATS');
}
// Wait for all processes to continue
while ($running = dsstats_processes_running($type)) {
dsstats_debug(sprintf('%s Processes Running, Sleeping for 2 seconds.', $running));
sleep(2);
}
}
/**
* dsstats_memory_limit - this routine increases/decreases the memory available for the script
* It is divided into two functions as the main dsstats poller calls this function directly
* as opposed to the call during the processing of poller output in the main cacti poller.
*
* @return void
*/
function dsstats_memory_limit() : void {
ini_set('memory_limit', read_config_option('dsstats_poller_mem_limit') . 'M');
}
/**
* dsstats_poller_bottom - This routine launches the main dsstats poller so that it might
* calculate the Hourly, Daily, Weekly, Monthly, and Yearly averages. It is forked independently
* to the Cacti poller after all polling has finished.
*
* @return void
*/
function dsstats_poller_bottom() : void {
if (read_config_option('dsstats_enable') == 'on') {
include_once(CACTI_PATH_LIBRARY . '/poller.php');
chdir(CACTI_PATH_BASE);
$command_string = read_config_option('path_php_binary');
if (read_config_option('path_dsstats_log') != '') {
if (CACTI_SERVER_OS == 'unix') {
$extra_args = '-q ' . CACTI_PATH_BASE . '/poller_dsstats.php >> ' . cacti_escapeshellarg(read_config_option('path_dsstats_log')) . ' 2>&1';
} else {
$extra_args = '-q ' . CACTI_PATH_BASE . '/poller_dsstats.php >> ' . cacti_escapeshellarg(read_config_option('path_dsstats_log'));
}
} else {
$extra_args = '-q ' . CACTI_PATH_BASE . '/poller_dsstats.php';
}
exec_background($command_string, $extra_args);
}
}
/**
* dsstats_rrdtool_init - This routine provides a bi-directional socket based connection to RRDtool.
* it provides a high speed connection to rrdfile in the case where the traditional Cacti call does
* not when performing fetch type calls.
*
* @return array An array that includes both the process resource and the pipes to communicate
* with RRDtool.
*/
function dsstats_rrdtool_init() : array {
if (CACTI_SERVER_OS == 'unix') {
$fds = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['file', '/dev/null', 'a'] // stderr
];
} else {
$fds = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['file', 'nul', 'a'] // stderr
];
}
// set the rrdtool default font
if (read_config_option('path_rrdtool_default_font')) {
putenv('RRD_DEFAULT_FONT=' . read_config_option('path_rrdtool_default_font'));
}
$command = read_config_option('path_rrdtool') . ' - ';
$process = proc_open($command, $fds, $pipes);
// proc_open returns false if RRDtool could not be spawned; without a process
// the pipes are not populated, so bail out before touching them
if (!is_resource($process)) {
cacti_log('ERROR: Unable to start RRDtool process for DSStats', false, 'DSSTATS');
return [false, false];
}
// make stdin/stdout/stderr non-blocking
stream_set_blocking($pipes[0], false);
stream_set_blocking($pipes[1], false);
return [$process, $pipes];
}
/**
* dsstats_rrdtool_execute - this routine passes commands to RRDtool and returns the information
* back to DSStats. It is important to note here that RRDtool needs to provide an either 'OK'
* or 'ERROR' response across the pipe as it does not provide EOF characters to key upon.
*
* This may not be the best method and may be changed after I have a conversation with a few
* developers.
*
* @param string $command The rrdtool command to execute
* @param array $rrd_process An array of stdin and stdout pipes to read and write data from
*
* @return string The output from RRDtool
*/
function dsstats_rrdtool_execute(string $command, array $rrd_process) : string {
static $broken = false;
$use_proxy = (intval(read_config_option('storage_location')) > 0 ? true : false);
if ($use_proxy) {
$stdout = rrdtool_execute($command, false, RRDTOOL_OUTPUT_STDOUT, $rrd_process, 'DSSTATS');
} else {
$stdout = '';
if ($command == '') {
return '';
}
$pipes = $rrd_process[1];
$command .= "\r\n";
$return_code = fwrite($pipes[0], $command);
if (is_resource($pipes[1])) {
while (!feof($pipes[1])) {
$stdout .= fgets($pipes[1], 4096);
if (str_contains($stdout, 'OK')) {
break;
}
if (str_contains($stdout, 'ERROR')) {
break;
}
}
} elseif (!$broken) {
cacti_log('ERROR: RRDtool was unable to fork. Likely RRDtool can not be found or system out of resources. Blocking subsequent messages.', false, 'POLLER');
$broken = true;
}
}
return $stdout;
}
/**
* dsstats_rrdtool_close - this routine closes the RRDtool process thus also
* closing the pipes.
*
* @param array $rrd_process
*
* @return void
*/
function dsstats_rrdtool_close(array $rrd_process) : void {
proc_terminate($rrd_process[0]);
proc_close($rrd_process[0]);
}
/**
* dsstats_launch_children - this function will launch collector children based upon
* the maximum number of threads and the process type
*
* @param string $type The process type
*
* @return int The number of children launched
*/
function dsstats_launch_children(string $type) : int {
global $debug;
$processes = read_config_option('dsstats_parallel');
if (empty($processes)) {
$processes = 1;
}
$php_binary = read_config_option('path_php_binary');
dsstats_debug("About to launch $processes processes.");
$sub_type = dsstats_get_subtype($type);
for ($i = 1; $i <= $processes; $i++) {
dsstats_debug(sprintf('Launching DSStats Process Number %s for Type %s', $i, $type));
cacti_log(sprintf('NOTE: Launching DSStats Process Number %s for Type %s', $i, $type), false, 'BOOST', POLLER_VERBOSITY_MEDIUM);
exec_background($php_binary, CACTI_PATH_BASE . "/poller_dsstats.php --type=$sub_type --child=$i" . ($debug ? ' --debug' : ''));
}
sleep(2);
return (int) $processes;
}
/**
* dsstats_get_subtype - this function determine the applicable
* sub-type (child name) and return if based upon a type
*
* @param string $type The process type
*
* @return string - The sub type
*/
function dsstats_get_subtype(string $type) : string {
switch($type) {
case 'master':
case 'pmaster':
return 'child';
case 'bmaster':
return 'bchild';
case 'dmaster':
return 'dchild';
}
return 'false';
}
/**
* dsstats_kill_running_processes - this function is part of an interrupt
* handler to kill children processes when the parent is killed
*
* @return void
*/
function dsstats_kill_running_processes() : void {
global $type;
// This function has always used unregister_process() from poller.php. Load
// that dependency explicitly so the newer identity check is available too,
// regardless of which entry point loaded dsstats.php.
require_once CACTI_PATH_LIBRARY . '/poller.php';
if ($type == 'bmaster') {
$processes = db_fetch_assoc_prepared('SELECT *
FROM processes
WHERE tasktype = "dsstats"
AND taskname = "bchild"
AND pid != ?',
[getmypid()]);
} else {
$processes = db_fetch_assoc_prepared('SELECT *
FROM processes
WHERE tasktype = "dsstats"
AND taskname IN ("child", "dchild")
AND pid != ?',
[getmypid()]);
}
if (cacti_sizeof($processes)) {
foreach ($processes as $p) {
if (cacti_process_still_running((int) $p['pid'])) {
cacti_log(sprintf('WARNING: Killing DSStats %s PID %d due to signal or overrun.', ucfirst($p['taskname']), $p['pid']), false, 'BOOST');
cacti_process_kill((int) $p['pid'], SIGTERM, 'BOOST');
}
unregister_process($p['tasktype'], $p['taskname'], $p['taskid'], $p['pid']);
}
}
}
/**
* dsstats_processes_running - given a type, determine the number
* of sub-type or children that are currently running
*
* @param string $type The process type
*
* @return int The number of running processes
*/
function dsstats_processes_running(string $type) : int {
$sub_type = dsstats_get_subtype($type);
$running = db_fetch_cell_prepared('SELECT COUNT(*)
FROM processes
WHERE tasktype = "dsstats"
AND taskname = ?',
[$sub_type]);
if ($running == 0) {
return 0;
}
return $running;
}
/**
* dsstats_get_best_partition - given a time range, determine the
* best partition to use to gather graph statistics
*
* @param int $start_time As a unix timestamp
* @param int $end_time As a unix timestamp
*
* @return string The table name of the best partition
*/
function dsstats_get_best_partition(int $start_time, int $end_time = 0) : string {
if ($end_time == 0) {
$end_time = time();
}
$cur_time = time();
$days = ceil(($end_time - $start_time) / 86400);
if ($days <= 1) {
// Daily partition
$desired_suffix = '_v' . date('Y', $start_time) . substr('000' . date('z'), -3);
$current_suffix = '_v' . date('Y', $cur_time) . substr('000' . date('z', $cur_time), -3);
if ($desired_suffix == $current_suffix) {
return 'data_source_stats_daily';
}
if (db_table_exists('data_source_stats_daily' . $desired_suffix)) {
return 'data_source_stats_daily' . $desired_suffix;
} else {
return 'data_source_stats_daily';
}
} elseif ($days > 1 && $days <= 7) {
// Weekly partition
$desired_suffix = '_v' . date('Y', $start_time) . substr('000' . date('W'), -3);
$current_suffix = '_v' . date('Y', $cur_time) . substr('000' . date('W', $cur_time), -3);
if ($desired_suffix == $current_suffix) {
return 'data_source_stats_weekly';
}
if (db_table_exists('data_source_stats_weekly' . $desired_suffix)) {
return 'data_source_stats_weekly' . $desired_suffix;
} else {
return 'data_source_stats_weekly';
}
} elseif ($days > 7 && $days <= 30) {
// Monthly partition
$desired_suffix = '_v' . date('Y', $start_time) . substr('000' . date('m'), -3);
$current_suffix = '_v' . date('Y', $cur_time) . substr('000' . date('m', $cur_time), -3);
if ($desired_suffix == $current_suffix) {
return 'data_source_stats_monthly';
}
if (db_table_exists('data_source_stats_monthly' . $desired_suffix)) {
return 'data_source_stats_monthly' . $desired_suffix;
} else {
return 'data_source_stats_monthly';
}
} else {
// Yearly partition
$desired_suffix = '_v' . date('Y', $start_time);
$current_suffix = '_v' . date('Y', $cur_time);
if ($desired_suffix == $current_suffix) {
return 'data_source_stats_yearly';
}
if (db_table_exists('data_source_stats_yearly' . $desired_suffix)) {
return 'data_source_stats_yearly' . $desired_suffix;
} else {
return 'data_source_stats_yearly';
}
}
}