Observium_CE/includes/rrdtool.inc.php

1257 lines
39 KiB
PHP

<?php
/**
* Observium
*
* This file is part of Observium.
*
* @package observium
* @subpackage rrdtool
* @copyright (C) 2006-2013 Adam Armstrong, (C) 2013-2022 Observium Limited
*
*/
/**
* Get full path for rrd file.
*
* @param array $device Device array
* @param string $filename Base filename for rrd file
* @return string Full rrd file path
*/
// TESTME needs unit testing
function get_rrd_path($device, $filename) {
global $config;
$rrd_dir = trim($config['rrd_dir']) . '/';
$filename = trim($filename);
if (str_starts($filename, $rrd_dir))
{
// Already full path
return $filename;
}
$filename = safename($filename);
// If filename empty, return base rrd dirname for device (for example in delete_device())
$rrd_file = $rrd_dir;
if (strlen($device['hostname'])) {
$rrd_file .= $device['hostname'] . '/';
}
if (!safe_empty($filename)) {
$path_array = explode('.', $filename);
if (!(count($path_array) > 1 && end($path_array) === 'rrd')) {
$filename .= '.rrd'; // Add rrd extension if not already set
}
//$ext = pathinfo($filename, PATHINFO_EXTENSION);
//if ($ext !== 'rrd') { $filename .= '.rrd'; }
$rrd_file .= $filename;
// Add rrd filename to global array $graph_return
$GLOBALS['graph_return']['rrds'][] = $rrd_file;
}
return $rrd_file;
}
/**
* Rename rrd file for device is some schema changes.
*
* @param array $device
* @param string $old_rrd Base filename for old rrd file
* @param string $new_rrd Base filename for new rrd file
* @param bool $overwrite Force overwrite new rrd file if already exist
* @return bool TRUE if renamed
*/
function rename_rrd($device, $old_rrd, $new_rrd, $overwrite = FALSE) {
$old_rrd = get_rrd_path($device, $old_rrd);
$new_rrd = get_rrd_path($device, $new_rrd);
print_debug_vars($old_rrd);
print_debug_vars($new_rrd);
if (rrd_is_file($old_rrd, TRUE)) {
if (!$overwrite && rrd_is_file($new_rrd, TRUE)) {
// If not forced overwrite file, return false
print_debug("RRD already exist new file: '$new_rrd'");
$renamed = FALSE;
} elseif (OBS_RRD_NOLOCAL) {
// Currently external rrd(cached) not support rename (also dump/restore), see:
// https://github.com/oetiker/rrdtool-1.x/issues/1141
// https://github.com/oetiker/rrdtool-1.x/issues/1142
$renamed = FALSE;
} else {
$renamed = rename($old_rrd, $new_rrd);
}
} else {
print_debug("RRD old file not found: '$old_rrd'");
$renamed = FALSE;
}
if ($renamed) {
print_debug("RRD moved: '$old_rrd' -> '$new_rrd'");
}
return $renamed;
}
/**
* Opens up a pipe to RRDTool using handles provided
*
* @return boolean
* @global array $config
* @param &rrd_process
* @param &rrd_pipes
*/
// TESTME needs unit testing
function rrdtool_pipe_open(&$rrd_process, &$rrd_pipes) {
global $config;
$command = $config['rrdtool'] . ' -'; // Waits for input via standard input (STDIN)
/*
$descriptorspec = array(
0 => array('pipe', 'r'), // stdin
1 => array('pipe', 'w'), // stdout
2 => array('pipe', 'w') // stderr
);
$cwd = $config['rrd_dir'];
$env = array();
$rrd_process = proc_open($command, $descriptorspec, $rrd_pipes, $cwd, $env);
*/
$rrd_process = pipe_open($command, $rrd_pipes, $config['rrd_dir']);
if (is_resource($rrd_process)) {
// $pipes now looks like this:
// 0 => writeable handle connected to child stdin
// 1 => readable handle connected to child stdout
// 2 => readable handle connected to child stderr
if (OBS_DEBUG > 1) {
print_message('RRD PIPE OPEN[%gTRUE%n]', 'console');
}
return TRUE;
}
if (isset($config['rrd']['debug']) && $config['rrd']['debug']) {
logfile('rrd.log', "RRD pipe process not opened '$command'.");
}
if (OBS_DEBUG > 1) {
print_message('RRD PIPE OPEN[%rFALSE%n]', 'console');
}
return FALSE;
}
/**
* Closes the pipe to RRDTool
*
* @return integer
* @param resource rrd_process
* @param array rrd_pipes
*/
// TESTME needs unit testing
function rrdtool_pipe_close($rrd_process, &$rrd_pipes) {
if (OBS_DEBUG > 1) {
$rrd_status['stdout'] = stream_get_contents($rrd_pipes[1]);
$rrd_status['stderr'] = stream_get_contents($rrd_pipes[2]);
}
/*
if (is_resource($rrd_pipes[0]))
{
fclose($rrd_pipes[0]);
}
fclose($rrd_pipes[1]);
fclose($rrd_pipes[2]);
// It is important that you close any pipes before calling
// proc_close in order to avoid a deadlock
$rrd_status['exitcode'] = proc_close($rrd_process);
*/
$rrd_status['exitcode'] = pipe_close($rrd_process, $rrd_pipes);
if (OBS_DEBUG > 1)
{
print_message('RRD PIPE CLOSE['.($rrd_status['exitcode'] !== 0 ? '%rFALSE' : '%gTRUE').'%n]', 'console');
if ($rrd_status['stdout'])
{
print_message("RRD PIPE STDOUT[\n".$rrd_status['stdout']."\n]", 'console', FALSE);
}
if ($rrd_status['exitcode'] && $rrd_status['stderr'])
{
// Show stderr if exitcode not 0
print_message("RRD PIPE STDERR[\n".$rrd_status['stderr']."\n]", 'console', FALSE);
}
}
return $rrd_status['exitcode'];
}
/**
* Generates a graph file at $graph_file using $options
* Opens its own rrdtool pipe.
*
* @return integer
* @param string graph_file
* @param string options
*/
// TESTME needs unit testing
function rrdtool_graph($graph_file, $options) {
global $config;
// Note, always use pipes, because standard command line has limits!
if ($config['rrdcached']) {
$options = str_replace($config['rrd_dir'].'/', '', $options);
$cmd = 'graph --daemon ' . $config['rrdcached'] . " $graph_file $options";
} else {
$cmd = "graph $graph_file $options";
}
// Prevent security hole by pass multiple commands from graph api
$cmd = str_replace([ "\r", "\n" ], '', $cmd);
$GLOBALS['rrd_status'] = FALSE;
$GLOBALS['exec_status'] = array('command' => $config['rrdtool'] . ' ' . $cmd,
'stdout' => '',
'exitcode' => -1);
$start = microtime(TRUE);
rrdtool_pipe_open($rrd_process, $rrd_pipes);
if (is_resource($rrd_process)) {
// $pipes now looks like this:
// 0 => writeable handle connected to child stdin
// 1 => readable handle connected to child stdout
// Any error output will be appended to /tmp/error-output.txt
/*
fwrite($rrd_pipes[0], $cmd);
fclose($rrd_pipes[0]);
$iter = 0;
while (strlen($line) < 1 && $iter < 1000) {
// wait for 10 milliseconds to loosen loop
usleep(10000);
$line = fgets($rrd_pipes[1], 1024);
$stdout .= $line;
$iter++;
}
$stdout = preg_replace('/(?:\n|\r\n|\r)$/D', '', $stdout); // remove last (only) eol
unset($iter);
*/
$stdout = pipe_read($cmd, $rrd_pipes, FALSE);
$runtime = microtime(TRUE) - $start;
// Check rrdtool's output for the command.
if (preg_match('/\d+x\d+/', $stdout)) {
$GLOBALS['rrd_status'] = TRUE;
} else {
$stderr = trim(stream_get_contents($rrd_pipes[2]));
if (isset($config['rrd']['debug']) && $config['rrd']['debug']) {
logfile('rrd.log', "RRD $stderr, CMD: " . $GLOBALS['exec_status']['command']);
}
}
$exitcode = rrdtool_pipe_close($rrd_process, $rrd_pipes);
$GLOBALS['exec_status']['exitcode'] = $exitcode;
$GLOBALS['exec_status']['stdout'] = $stdout;
$GLOBALS['exec_status']['stderr'] = $stderr;
} else {
$runtime = microtime(TRUE) - $start;
$stdout = NULL;
}
$GLOBALS['exec_status']['runtime'] = $runtime;
// Add some data to global array $graph_return
$GLOBALS['graph_return']['status'] = $GLOBALS['rrd_status'];
$GLOBALS['graph_return']['command'] = $GLOBALS['exec_status']['command'];
$GLOBALS['graph_return']['filename'] = $graph_file;
$GLOBALS['graph_return']['output'] = $stdout;
$GLOBALS['graph_return']['runtime'] = $GLOBALS['exec_status']['runtime'];
if (OBS_DEBUG) {
print_message(PHP_EOL . 'RRD CMD[%y' . $cmd . '%n]', 'console', FALSE);
$debug_msg = 'RRD RUNTIME['.($runtime > 0.1 ? '%r' : '%g').round($runtime, 4).'s%n]' . PHP_EOL;
$debug_msg .= 'RRD STDOUT['.($GLOBALS['rrd_status'] ? '%g': '%r').$stdout.'%n]' . PHP_EOL;
if ($stderr) {
$debug_msg .= 'RRD STDERR[%r'.$stderr.'%n]' . PHP_EOL;
}
$debug_msg .= 'RRD_STATUS['.($GLOBALS['rrd_status'] ? '%gTRUE': '%rFALSE').'%n]';
print_message($debug_msg . PHP_EOL, 'console');
}
return $stdout;
}
/**
* Generates and pipes a command to rrdtool
*
* @param string command
* @param string filename
* @param string options
* @global array $config
* @global mixed $rrd_pipes
*/
// TESTME needs unit testing
function rrdtool($command, $filename, $options)
{
global $config, $rrd_pipes;
$fsfilename = $filename; // keep full filename
// Remote RRDcached require minimum version 1.5.5
// Do not use rrdcached on local hosted rrd, for some commands
if ($config['rrdcached'] && (OBS_RRD_NOLOCAL || !in_array($command, [ 'create', 'tune', 'info', 'last', 'lastupdate' ]))) {
$filename = str_replace($config['rrd_dir'] . '/', '', $filename);
if (OBS_RRD_NOLOCAL && $command === 'create') {
// No overwrite for remote rrdtool, since no way for check if rrdfile exist
$options .= ' --no-overwrite';
}
$options .= ' --daemon ' . $config['rrdcached'];
}
$cmd = "$command $filename $options";
$GLOBALS['rrd_status'] = FALSE;
$GLOBALS['exec_status'] = [
'command' => $config['rrdtool'] . ' ' . $cmd,
'exitcode' => 1
];
if ($config['norrd']) {
print_message("[%rRRD Disabled - $cmd%n]", 'color');
return NULL;
}
if (in_array($command, [ 'fetch', 'last', 'lastupdate', 'tune', 'xport', 'dump', 'restore', 'info' ])) {
// This commands require exact STDOUT, skip use pipes
//$command = $config['rrdtool'] . ' ' . $cmd;
$stdout = external_exec($config['rrdtool'] . ' ' . $cmd, 500); // Limit exec time to 500ms
$runtime = $GLOBALS['exec_status']['runtime'];
$GLOBALS['rrd_status'] = $GLOBALS['exec_status']['exitcode'] === 0;
// Check rrdtool's output for the command.
if (!$GLOBALS['rrd_status'] && isset($config['rrd']['debug']) && $config['rrd']['debug']) {
logfile('rrd.log', "RRD ".$GLOBALS['exec_status']['stderr'].", CMD: $cmd");
}
} else {
// FIXME, need add check if pipes exist
$start = microtime(TRUE);
fwrite($rrd_pipes[0], $cmd."\n");
// FIXME, complete not sure why sleep required here.. @mike
usleep(1000);
$stdout = trim(stream_get_contents($rrd_pipes[1]));
$stderr = trim(stream_get_contents($rrd_pipes[2]));
$runtime = microtime(TRUE) - $start;
// Check rrdtool's output for the command.
if (str_contains($stdout, 'ERROR')) {
if (isset($config['rrd']['debug']) && $config['rrd']['debug']) {
logfile('rrd.log', "RRD $stdout, CMD: $cmd");
}
} else {
$GLOBALS['rrd_status'] = TRUE;
$GLOBALS['exec_status']['exitcode'] = 0;
}
$GLOBALS['exec_status']['stdout'] = $stdout;
$GLOBALS['exec_status']['stderr'] = $stderr;
$GLOBALS['exec_status']['runtime'] = $runtime;
}
$GLOBALS['rrdtool'][$command]['time'] += $runtime;
$GLOBALS['rrdtool'][$command]['count']++;
if ($command === 'update' && !$GLOBALS['rrd_status'] &&
isset($config['rrd']['debug']) && $config['rrd']['debug'] &&
str_contains($stdout, 'Permission denied')) {
// no rrdcached: ERROR: opening '/opt/observium/rrd/hostname/perf-pollersnmp_errors.rrd': Permission denied
// rrdcached: ERROR: rrdcached: Cannot read/write /opt/observium/rrd/hostname/perf-pollersnmp_errors.rrd: Permission denied
// get device by filepath
list($hostname) = explode('/', str_replace($config['rrd_dir'] . '/', '', $fsfilename), 2);
$device = device_by_name($hostname);
// Eventlog incorrect write permissions
//log_event("RRD file '".basename($fsfilename)."' is not writeable for ".OBS_PROCESS_NAME." process", $device, 'device', $device['device_id'], 7);
logfile('rrd.log', "RRD file '".basename($fsfilename)."' is not writeable for ".OBS_PROCESS_NAME." process");
}
if (OBS_DEBUG) {
print_message(PHP_EOL . 'RRD CMD[%y' . $cmd . '%n]', 'console', FALSE);
$debug_msg = 'RRD RUNTIME['.($runtime > 1 ? '%r' : '%g').round($runtime, 4).'s%n]' . PHP_EOL;
$debug_msg .= 'RRD STDOUT['.($GLOBALS['rrd_status'] ? '%g': '%r').$stdout.'%n]' . PHP_EOL;
if ($stderr) {
$debug_msg .= 'RRD STDERR[%r'.$stderr.'%n]' . PHP_EOL;
}
$debug_msg .= 'RRD_STATUS['.($GLOBALS['rrd_status'] ? '%gTRUE': '%rFALSE').'%n]';
print_message($debug_msg . PHP_EOL, 'console');
if ($command === 'tune') { rrdtool_file_info($filename); }
}
return $stdout;
}
/**
* Generates an rrd database at $filename using $options
* Creates the file if it does not exist yet.
* DEPRECATED: use rrdtool_create_ng(), this will disappear and ng will be renamed when conversion is complete.
*
* @param array device
* @param string filename
* @param string ds
* @param string options
*/
function rrdtool_create($device, $filename, $ds, $options = '') {
global $config;
if ($filename[0] === '/') {
print_debug("You should pass the filename only (not the full path) to this function! Passed filename: ".$filename);
$filename = basename($filename);
}
$fsfilename = get_rrd_path($device, $filename);
if ($config['norrd']) {
print_message("[%rRRD Disabled - create $fsfilename%n]", 'color');
return NULL;
}
if (OBS_RRD_NOLOCAL && !$config['cache']['enable_cli']) {
// do not check remote without caching
} elseif (rrd_exists($device, $filename)) {
print_debug("RRD $fsfilename already exists - no need to create.");
return FALSE; // Bail out if the file exists already
}
if (OBS_RRD_NOLOCAL) {
print_debug("RRD create $fsfilename passed to remote rrdcached with --no-overwrite.");
}
if (!$options) {
$options = preg_replace('/\s+/', ' ', $config['rrd']['rra']);
}
$step = "--step ".$config['rrd']['step'];
//$command = $config['rrdtool'] . " create $fsfilename $ds $step $options";
//return external_exec($command);
// Clean up old ds strings. This is kinda nasty.
$ds = str_replace("\
", '', $ds);
return rrdtool('create', $fsfilename, $ds . " $step $options");
}
/**
* Generates RRD filename from definition
*
* @param string/array $def Original filename, using %index% (or %custom% %keys%) as placeholder for indexes
* @param string/array $index Index, if RRD type is indexed (or array of multiple indexes)
* @return string Filename of RRD
*/
// TESTME needs unit testing
function rrdtool_generate_filename($def, $index) {
if (is_string($def)) {
// Compat with old
$filename = $def;
} elseif (isset($def['file'])) {
$filename = $def['file'];
} elseif (isset($def['entity_type'])) {
// Entity specific filename by ID, ie for sensor/status/counter
$entity_id = $index;
return get_entity_rrd_by_id($def['entity_type'], $entity_id);
}
// Generate warning for indexed filenames containing %index% - does not help if you use custom field names for indexing
if (str_contains($filename, '%index%') && safe_empty($index)) {
print_warning("RRD filename generation error: filename contains %index%, but \$index is NULL!");
}
// Convert to single element array if not an array.
// This will automatically use %index% as the field to replace (see below).
if (!is_array($index)) { $index = array('index' => $index); }
// Replace %index% by $index['index'], %foo% by $index['foo'] etc.
$filename = array_tag_replace($index, $filename);
return safename($filename);
}
/**
* Generates an rrd database based on $type definition, using $options
* Only creates the file if it does not exist yet.
* Should most likely not be called on its own, as an update call will check for existence.
*
* @param array $device Device array
* @param string/array $type rrd file type from $config['rrd_types'] or actual config array
* @param string/array $index Index, if RRD type is indexed (or array of multiple tags)
* @param array $options Options for create RRD, like STEP, RRA, MAX or SPEED
*
* @return string
*/
// TESTME needs unit testing
function rrdtool_create_ng($device, $type, $index = NULL, $options = []) {
global $config;
if (!is_array($type)) { // We were passed a string
if (!is_array($config['rrd_types'][$type])) { // Check if definition exists
print_warning("Cannot create RRD for type $type - not found in definitions!");
return FALSE;
}
$definition = $config['rrd_types'][$type];
} else { // We were passed an array, use as-is
$definition = $type;
}
$filename = rrdtool_generate_filename($definition, $index);
$fsfilename = get_rrd_path($device, $filename);
if ($config['norrd']) {
print_message("[%rRRD Disabled - create $fsfilename%n]", 'color');
return NULL;
}
if (OBS_RRD_NOLOCAL && !$config['cache']['enable_cli']) {
// do not check remote without caching
} elseif (rrd_exists($device, $filename)) {
print_debug("RRD $fsfilename already exists - no need to create.");
return FALSE; // Bail out if the file exists already
}
if (OBS_RRD_NOLOCAL) {
print_debug("RRD create $fsfilename passed to remote rrdcached with --no-overwrite.");
}
// Set RRA option
$rra = isset($options['rra']) ? $options['rra'] : $config['rrd']['rra'];
$rra = preg_replace('/\s+/', ' ', $rra);
// Set step
$step = isset($options['step']) ? $options['step'] : $config['rrd']['step'];
// Create tags, for use in replace
$tags = [];
if (strlen((string)$index)) {
$tags['index'] = $index;
}
if (isset($options['speed'])) {
print_debug("Passed speed: ".$options['speed']);
$options['speed'] = (int)(unit_string_to_numeric($options['speed']) / 8); // Detect passed speed value (converted to bits)
$tags['speed'] = max($options['speed'], $config['max_port_speed']); // But result select maximum between passed and default!
print_debug(" RRD speed: ".$options['speed'].PHP_EOL.
" Default: ".$config['max_port_speed'].PHP_EOL.
" Max: ".$tags['speed']);
} else {
// Default speed
$tags['speed'] = $config['max_port_speed'];
}
// Create DS parameter based on the definition
$ds = array();
foreach ($definition['ds'] as $name => $def)
{
if (strlen($name) > 19) { print_warning("SEVERE: DS name $name is longer than 19 characters - over RRD limit!"); }
// Set defaults for missing attributes
if (!isset($def['type'])) { $def['type'] = 'COUNTER'; }
if (!isset($def['max'])) { $def['max'] = 'U'; }
else { $def['max'] = array_tag_replace($tags, $def['max']); } // can use %speed% tag, speed must passed by $options['speed']
if (!isset($def['min'])) { $def['min'] = 'U'; }
if (!isset($def['heartbeat'])) { $def['heartbeat'] = 2 * $step; }
// Create DS string to pass on the command line
$ds[] = "DS:$name:" . $def['type'] . ':' . $def['heartbeat'] . ':' . $def['min'] . ':' . $def['max'];
}
return rrdtool('create', $fsfilename, implode(' ', $ds) . " --step $step $rra");
}
/**
* Updates an rrd database at $filename using $options
* Where $options is an array, each entry which is not a number is replaced with "U"
*
* @param array $device Device array
* @param string/array $type RRD file type from $config['rrd_types'] or actual config array
* @param array $ds DS data (key/value)
* @param string/array $index Index, if RRD type is indexed (or array of multiple indexes)
* @param bool $create Create RRD file if it does not exist
* @param array $options Options to pass to create function if file does not exist
*
* @return string
*/
// TESTME needs unit testing
function rrdtool_update_ng($device, $type, $ds, $index = NULL, $create = TRUE, $options = []) {
global $config, $graphs;
if (!is_array($type)) { // We were passed a string
if (!is_array($config['rrd_types'][$type])) { // Check if definition exists
print_warning("Cannot create RRD for type $type - not found in definitions!");
return FALSE;
}
$definition = $config['rrd_types'][$type];
// Append graph if not already passed
if (!isset($definition['graphs'])) {
$definition['graphs'] = array(str_replace('-', '_', $type));
}
} else { // We were passed an array, use as-is
$definition = $type;
}
$filename = rrdtool_generate_filename($definition, $index);
$fsfilename = get_rrd_path($device, $filename);
// Create the file if missing (if we have permission to create it)
if ($create) {
rrdtool_create_ng($device, $type, $index, $options);
}
// Update DSes when requested
if (isset($options['update_max']) || isset($options['update_min'])) {
print_debug_vars($options);
rrdtool_update_ds($device, $type, $index, $options);
}
$update = array('N');
foreach ($definition['ds'] as $name => $def) {
if (isset($ds[$name])) {
if (is_numeric($ds[$name])) {
// Add data to DS update string
$update[] = $ds[$name];
} else {
// Data not numeric, mark unknown
$update[] = 'U';
}
} else {
// Data not sent, mark unknown
$update[] = 'U';
}
}
/** // This is setting loads of random shit that doesn't exist
// ONLY GRAPHS THAT EXIST MAY GO INTO THIS ARRAY
// Set global graph variable for store avialable device graphs
foreach ($definition['graphs'] as $def)
{
$graphs[$def] = TRUE;
}
**/
if ($config['influxdb']['enabled'])
{
influxdb_update($device, $filename, $ds, $definition, $index);
}
return rrdtool('update', $fsfilename, implode(':', $update));
}
/**
* Updates an rrd database at $filename using $options
* Where $options is an array, each entry which is not a number is replaced with "U"
* DEPRECATED: use rrdtool_update_ng(), this will disappear and ng will be renamed when conversion is complete.
*
* @param array $device
* @param string $filename
* @param array $options
* @return string
*/
function rrdtool_update($device, $filename, $options)
{
// Do some sanitization on the data if passed as an array.
if (is_array($options))
{
$values[] = "N";
foreach ($options as $value)
{
if (!is_numeric($value)) { $value = 'U'; }
$values[] = $value;
}
$options = implode(':', $values);
}
if ($filename[0] == '/')
{
$filename = basename($filename);
print_debug("You should pass the filename only (not the full path) to this function!");
}
$fsfilename = get_rrd_path($device, $filename);
if ($GLOBALS['config']['influxdb']['enabled'])
{
influxdb_update( $device, $filename, $options );
}
return rrdtool("update", $fsfilename, $options);
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function rrdtool_fetch($filename, $options) {
return rrdtool('fetch', $filename, $options);
}
// TESTME needs unit testing
/**
* Returns the UNIX timestamp of the most recent update of $filename
*
* @param string $filename RRD filename
* @param string $options Mostly not required
* @return string UNIX timestamp
*/
function rrdtool_last($filename, $options = '') {
return rrdtool('last', $filename, $options);
}
// TESTME needs unit testing
/**
* Returns the UNIX timestamp and the value stored for each datum in the most recent update of $filename
*
* @param string $filename RRD filename
* @param string $options Mostly not required
* @return string UNIX timestamp and the value stored for each datum
*/
function rrdtool_lastupdate($filename, $options = '') {
return rrdtool('lastupdate', $filename, $options);
}
/**
* Checks if an RRD database at $filename for $device exists
* Checks via rrdcached if configured, else via is_exists
*
* @param array $device
* @param string $filename
**/
function rrd_exists($device, $filename) {
//global $config;
$fsfilename = get_rrd_path($device, $filename);
// Return cached variant of rrdfile is exist
return rrd_is_file($fsfilename, TRUE);
}
/**
* Pass is_file() on local system
* and rrdtool last on REMOTE rrdcached
* or pass TRUE if $remote_validate == FALSE
*
* @param string $filename RRD filename
* @param bool $remote_validate Validate if RRD file exist with remote rrdcached
* @return bool
*/
function rrd_is_file($filename, $remote_validate = FALSE) {
if (OBS_RRD_NOLOCAL) {
// NOTE. RRD last on remote daemon reduce polling times
if ($remote_validate) {
if (!$GLOBALS['config']['rrd']['cache']) {
// Extra way for skip use phpFastCache in polling
// WARNING. Don't use this until you know what you are doing
print_debug("Remote RRDcacheD caching disabled for rrd_is_file('$filename', TRUE) call.");
$unixtime = rrdtool_last($filename);
if (is_numeric($unixtime) && $unixtime > OBS_MIN_UNIXTIME) {
// Correct unixtime without errors
return TRUE;
}
//ERROR: realpath(hostname/status.rrd): No such file or directory
return strpos($GLOBALS['exec_status']['stderr'], 'No such file') === FALSE;
//return $GLOBALS['rrd_status'];
}
$cache_key = 'rrd_is_file-' . str_replace($GLOBALS['config']['rrd_dir'].'/', '', $filename);
$cache_item = get_cache_item($cache_key);
if (!ishit_cache_item($cache_item)) {
$unixtime = rrdtool_last($filename);
if (is_numeric($unixtime) && $unixtime > OBS_MIN_UNIXTIME) {
// Correct unixtime without errors
$exist = TRUE;
} else {
//ERROR: realpath(hostname/status.rrd): No such file or directory
$exist = strpos($GLOBALS['exec_status']['stderr'], 'No such file') === FALSE;
//return $GLOBALS['rrd_status'];
}
// Store $cache in fast caching
set_cache_item($cache_item, $exist ? 1 : 0, [ 'ttl' => 3600 ]); // set valid for 1 hour
//print_debug_vars(get_cache_items('__cli'));
return $exist;
}
// Cached item
$exist = get_cache_data($cache_item);
if (OBS_DEBUG || (defined('OBS_CACHE_DEBUG') && OBS_CACHE_DEBUG)) {
print_message("RRD file '%W$filename%n' exist cached [".($exist ? '%gTRUE%n' : '%yFALSE%n')."].", 'console');
//print_vars($exist);
}
return (bool)$exist;
}
// Probably for polling
return TRUE;
}
// Local system, just use file valid check
return is_file($filename);
}
function rrdtool_file_valid($file) {
global $config;
if (OBS_RRD_NOLOCAL) {
print_message('[%gRRD REMOTE UNSUPPORTED%n] ');
return TRUE;
}
if (!is_file($file)) {
return FALSE;
}
// Just validate
$unixtime = rrdtool_last($file);
if (is_numeric($unixtime) && $unixtime > OBS_MIN_UNIXTIME) {
// Correct unixtime without errors
return TRUE;
}
print_debug("\nrrdtool_last($file):\n");
print_debug_vars($unixtime, 1);
// print_cli("\nrrdtool_last($file):\n");
// print_vars($unixtime, 1);
// print_vars($GLOBALS['exec_status']['stderr']);
$info = external_exec("/usr/bin/env file -b " . $file);
if (str_starts($info, 'RRDTool DB')) {
return TRUE;
}
print_debug("\nfile -b $file:\n");
print_debug_vars($info, 1);
// print_cli("\nfile -b $file:\n");
// print_vars($info, 1);
return FALSE;
//$rrd = external_exec($config['rrdtool'] . ' info -F ' . $file); // -F not exist for old rrdtool 1.4.8
//return $rrd && !str_contains($rrd, 'is not an RRD file');
}
/**
* Renames a DS inside an RRD file
*
* @param array $device Device
* @param string $filename Filename
* @param string $options Mostly not required
*
* @return bool|string|string[]|null
*/
function rrdtool_export_ng($device, $filename, $options = '', $start = NULL, $end = NULL, $rows = NULL) {
$fsfilename = get_rrd_path($device, $filename);
// https://oss.oetiker.ch/rrdtool/doc/rrdfetch.en.html#AT-STYLE_TIME_SPECIFICATION
// Oct 12 -- October 12 this year
// -1month or -1m -- current time of day, only a month before (may yield surprises, see NOTE3 above).
// noon yesterday -3hours -- yesterday morning; can also be specified as 9am-1day.
// 23:59 31.12.1999 -- 1 minute to the year 2000.
// 12/31/99 11:59pm -- 1 minute to the year 2000 for imperialists.
// 12am 01/01/01 -- start of the new millennium
// end-3weeks or e-3w -- 3 weeks before end time (may be used as start time specification).
// start+6hours or s+6h -- 6 hours after start time (may be used as end time specification).
// 931200300 -- 18:45 (UTC), July 5th, 1999 (yes, seconds since 1970 are valid as well).
// 19970703 12:45 -- 12:45 July 3th, 1997 (my favorite, and it has even got an ISO number (8601)).
if (strlen($start)) {
$options .= ' -s '.escapeshellarg($start);
}
if (strlen($end)) {
$options .= ' -e '.escapeshellarg($end);
}
if (strlen($rows)) {
$options .= ' -m '.escapeshellarg($rows);
}
return rrdtool_export($fsfilename, $options);
}
function rrdtool_export($filename, $options = '') {
global $config;
$return = FALSE;
if ($config['norrd']) {
print_message('[%gRRD Disabled%n] ');
return $return;
}
// rrdtool tune rename DS supported since v1.4
$version = get_versions();
if (version_compare($version['rrdtool_version'], '1.4.6', '<')) {
print_error('[%gRRD too old. JSON supported since 1.4.6%n] ');
return $return;
}
//$fsfilename = get_rrd_path($device, $filename);
print_vars($filename);
return rrdtool('xport', $filename, "--json -t ".$options);
}
// TESTME needs unit testing
/**
* Renames a DS inside an RRD file
*
* @param array $device Device
* @param string $filename Filename
* @param string $oldname Current DS name
* @param string $newname New DS name
*/
function rrdtool_rename_ds($device, $filename, $oldname, $newname)
{
global $config;
$return = FALSE;
if ($config['norrd'])
{
print_message('[%gRRD Disabled%n] ');
return $return;
}
// rrdtool tune rename DS supported since v1.4
// really not, see:
// https://github.com/oetiker/rrdtool-1.x/pull/1139
$version = get_versions();
if (version_compare($version['rrdtool_version'], '1.4', '>=')) {
$fsfilename = get_rrd_path($device, $filename);
print_debug("RRD DS renamed, file $fsfilename: '$oldname' -> '$newname'");
return rrdtool('tune', $filename, "--data-source-rename $oldname:$newname");
}
// Comparability with old version (but we support only >= v1.5.5, this not required)
if (OBS_RRD_NOLOCAL)
{
print_message('[%gRRD REMOTE UNSUPPORTED%n] ');
} else {
$fsfilename = get_rrd_path($device, $filename);
if (is_file($fsfilename))
{
// this function used in discovery, where not exist rrd pipes
$command = $config['rrdtool'] . " tune $fsfilename --data-source-rename $oldname:$newname";
$return = external_exec($command);
//print_vars($GLOBALS['exec_status']);
if ($GLOBALS['exec_status']['exitcode'] === 0)
{
print_debug("RRD DS renamed, file $fsfilename: '$oldname' -> '$newname'");
} else {
$return = FALSE;
}
}
}
return $return;
}
// TESTME needs unit testing
/**
* Adds a DS to an RRD file
*
* @param array Device
* @param string Filename
* @param string New DS name
*/
function rrdtool_add_ds($device, $filename, $add)
{
global $config;
$return = FALSE;
if ($config['norrd'])
{
print_message("[%gRRD Disabled%n] ");
return $return;
}
// rrdtool tune add DS supported since v1.4
$version = get_versions();
if (version_compare($version['rrdtool_version'], '1.4', '>='))
{
$fsfilename = get_rrd_path($device, $filename);
print_debug("RRD DS added, file ".$fsfilename.": '".$add."'");
return rrdtool('tune', $filename, "DS:$add");
}
// Comparability with old version (but we support only >= v1.5.5, this not required)
if (OBS_RRD_NOLOCAL)
{
print_message('[%gRRD REMOTE UNSUPPORTED%n] ');
} else {
$fsfilename = get_rrd_path($device, $filename);
if (is_file($fsfilename))
{
// this function used in discovery, where not exist rrd pipes
$fsfilename = get_rrd_path($device, $filename);
$return = external_exec($config['install_dir'] . "/scripts/add_ds_to_rrd.pl ".dirname($fsfilename)." ".basename($fsfilename)." $add");
//print_vars($GLOBALS['exec_status']);
if ($GLOBALS['exec_status']['exitcode'] === 0)
{
print_debug("RRD DS added, file ".$fsfilename.": '".$add."'");
} else {
$return = FALSE;
}
}
}
return $return;
}
function rrdtool_update_ds($device, $type, $index = NULL, $options = []) {
global $config;
if (!is_array($type)) { // We were passed a string
if (!is_array($config['rrd_types'][$type])) { // Check if definition exists
print_warning("Cannot update DSes in RRD for type $type - not found in definitions!");
return FALSE;
}
$definition = $config['rrd_types'][$type];
} else { // We were passed an array, use as-is
$definition = $type;
}
$filename = rrdtool_generate_filename($definition, $index);
$fsfilename = get_rrd_path($device, $filename);
if ($config['norrd']) {
print_message("[%rRRD Disabled - update rra $fsfilename%n]", 'color');
return NULL;
}
if (!rrd_exists($device, $filename)) {
print_debug("RRD $fsfilename not exist - no need to update.");
return FALSE; // Bail out if the file exists already
}
print_debug("RRD update DSes requested.");
// Create tags, for use in replace
$tags = [];
if (strlen((string)$index)) {
$tags['index'] = $index;
}
if (isset($options['speed'])) {
print_debug("Passed speed: ".$options['speed']);
$options['speed'] = (int)(unit_string_to_numeric($options['speed']) / 8); // Detect passed speed value (converted to bits)
$tags['speed'] = max($options['speed'], $config['max_port_speed']); // But result select maximum between passed and default!
print_debug(" RRD speed: ".$options['speed'].PHP_EOL.
" Default: ".$config['max_port_speed'].PHP_EOL.
" Max: ".$tags['speed']);
} else {
// Default speed
$tags['speed'] = $config['max_port_speed'];
}
// Create DS parameter based on the definition
$update = [];
foreach ($definition['ds'] as $name => $def)
{
if (strlen($name) > 19) { print_warning("SEVERE: DS name $name is longer than 19 characters - over RRD limit!"); }
// Set defaults for missing attributes
if (!isset($def['type'])) { $def['type'] = 'COUNTER'; }
if (!isset($def['max'])) { $def['max'] = 'U'; }
else { $def['max'] = array_tag_replace($tags, $def['max']); } // can use %speed% tag, speed must passed by $options['speed']
if (!isset($def['min'])) { $def['min'] = 'U'; }
if (!isset($def['heartbeat'])) { $def['heartbeat'] = 2 * $step; }
if (isset($options['update_min']) && $options['update_min']) {
$update[] = "--minimum $name:".$def['min'];
}
if (isset($options['update_max']) && $options['update_max']) {
$update[] = "--maximum $name:".$def['max'];
}
}
if (safe_count($update)) {
if (OBS_DEBUG) { rrdtool_file_info($fsfilename); }
//return TRUE;
return rrdtool('tune', $fsfilename, implode(' ', $update));
}
return FALSE;
}
// TESTME needs unit testing
/**
* Adds one or more RRAs to an RRD file; space-separated if you want to add more than one.
*
* @param array Device
* @param string Filename
* @param array RRA(s) to be added to the RRD file
*/
function rrdtool_add_rra($device, $filename, $options)
{
global $config;
if ($config['norrd'])
{
print_message('[%gRRD Disabled%n] ');
}
elseif (OBS_RRD_NOLOCAL)
{
///FIXME Currently unsupported on remote rrdcached
print_message('[%gRRD REMOTE UNSUPPORTED%n] ');
} else {
$fsfilename = get_rrd_path($device, $filename);
external_exec($config['install_dir'] . "/scripts/rrdtoolx.py addrra $fsfilename $fsfilename.new $options");
rename("$fsfilename.new", $fsfilename);
}
}
/**
* Escapes strings for RRDtool
*
* @param string $string String to escape
* @param integer $maxlength if passed, string will be padded and trimmed to exactly this length (after rrdtool unescapes it)
*
* @return string Escaped string
*/
// TESTME needs unit testing
function rrdtool_escape($string, $maxlength = NULL) {
if (is_numeric($maxlength)) {
$string = substr(str_pad($string, $maxlength), 0, $maxlength);
}
$from = [ ':', "'", '%', "\r", "\n" ];
$to = [ '\:', '`', '%%', '', '' ];
// FIXME: should maybe also probably escape these? # \ ? [ ^ ] ( $ ) '
return str_replace($from, $to, $string);
}
/**
* Helper function to strip quotes from RRD output
*
* @str RRD-Info generated string
* @return String with one surrounding pair of quotes stripped
*/
// TESTME needs unit testing
function rrd_strip_quotes($str)
{
if ($str[0] == '"' && $str[strlen($str)-1] == '"')
{
return substr($str, 1, strlen($str)-2);
}
return $str;
}
/**
* Determine useful information about RRD file
*
* Copyright (C) 2009 Bruno Prémont <bonbons AT linux-vserver.org>
*
* @file Name of RRD file to analyse
*
* @param string $file
*
* @return array Array describing the RRD file
*/
// TESTME needs unit testing
function rrdtool_file_info($file) {
global $config;
$info = [ 'filename' => $file ];
$out = rrdtool('info', $file, '');
if (!$out) {
return $info;
}
// if (OBS_RRD_NOLOCAL) {
// ///FIXME Currently unsupported on remote rrdcached
// print_message('[%gRRD REMOTE UNSUPPORTED%n] ');
// return $info;
// }
//$rrd = array_filter(explode(PHP_EOL, external_exec($config['rrdtool'] . ' info ' . $file)), 'strlen');
$rrd = array_filter(explode(PHP_EOL, $out), 'strlen');
if ($rrd) {
foreach ($rrd as $s)
{
$p = strpos($s, '=');
if ($p === false)
{
continue;
}
$key = trim(substr($s, 0, $p));
$value = trim(substr($s, $p+1));
if (strncmp($key,'ds[', 3) == 0)
{
/* DS definition */
$p = strpos($key, ']');
$ds = substr($key, 3, $p-3);
if (!isset($info['DS']))
{
$info['DS'] = array();
}
$ds_key = substr($key, $p+2);
if (strpos($ds_key, '[') === false)
{
if (!isset($info['DS']["$ds"]))
{
$info['DS']["$ds"] = array();
}
$info['DS']["$ds"]["$ds_key"] = rrd_strip_quotes($value);
}
}
elseif (strncmp($key, 'rra[', 4) == 0)
{
/* RRD definition */
$p = strpos($key, ']');
$rra = substr($key, 4, $p-4);
if (!isset($info['RRA']))
{
$info['RRA'] = array();
}
$rra_key = substr($key, $p+2);
if (strpos($rra_key, '[') === false)
{
if (!isset($info['RRA']["$rra"]))
{
$info['RRA']["$rra"] = array();
}
$info['RRA']["$rra"]["$rra_key"] = rrd_strip_quotes($value);
}
} elseif (strpos($key, '[') === false) {
$info[$key] = rrd_strip_quotes($value);
}
}
}
return $info;
}
// Creates a string of X number of ,ADDNAN. Used when aggregating things.
function rrd_addnan($count)
{
return str_repeat(',ADDNAN', $count);
}
// creates an rpn string to add an array of DSes together
function rrd_aggregate_dses($ds_list) {
if (!is_array($ds_list)) { return ''; }
return implode(',', $ds_list) . rrd_addnan(count($ds_list) - 1);
}
// EOF