#!/usr/bin/env php 1) { print 'FATAL: This utility is designed for the main Data Collector only' . PHP_EOL; exit(1); } } if (!function_exists('cacti_sizeof')) { function cacti_sizeof(mixed $array) : int { return ($array === false || !is_array($array)) ? 0 : sizeof($array); } function cacti_count(mixed $array) : int { return ($array === false || !is_array($array)) ? 0 : count($array); } } if (!function_exists('get_cacti_cli_version')) { function get_cacti_cli_version() : string { return (string) db_fetch_cell('SELECT cacti FROM version'); } } if (!function_exists('is_resource_writable')) { function is_resource_writable(string $path) : bool { if ($path[strlen($path) - 1] == '/') { return is_resource_writable($path . uniqid('', true) . '.tmp'); } if (file_exists($path)) { if (($f = @fopen($path, 'a'))) { fclose($f); return true; } return false; } if (($f = @fopen($path, 'w'))) { fclose($f); unlink($path); return true; } return false; } } ini_set('max_execution_time', '0'); ini_set('display_errors', 'On'); // setup defaults $debug = false; $dryrun = false; $backup = false; $overwrite = false; $user = get_current_user(); $owner = 'apache'; $ownerset = false; $oldrrd = ''; $newrrd = ''; $finrrd = ''; $time = microtime(true); global $debug; // process calling arguments $parms = $_SERVER['argv']; array_shift($parms); if (cacti_sizeof($parms)) { foreach ($parms as $parameter) { if (str_contains($parameter, '=')) { [$arg, $value] = explode('=', $parameter, 2); } else { $arg = $parameter; $value = ''; } switch ($arg) { case '--oldrrd': $oldrrd = $value; if (!file_exists($oldrrd)) { print 'FATAL: File \'' . $oldrrd . '\' does not exist.' . PHP_EOL; exit(-9); } if (!is_resource_writable($oldrrd)) { print 'FATAL: File \'' . $oldrrd . '\' is not writable by this account.' . PHP_EOL; exit(-8); } break; case '--newrrd': $newrrd = $value; if (!file_exists($newrrd)) { print 'FATAL: File \'' . $newrrd . '\' does not exist.' . PHP_EOL; exit(-9); } if (!is_resource_writable($newrrd)) { print 'FATAL: File \'' . $newrrd . '\' is not writable by this account.' . PHP_EOL; exit(-8); } break; case '--finrrd': $finrrd = $value; if (!is_resource_writable(dirname($finrrd) . '/') || (file_exists($finrrd) && !is_resource_writable($finrrd))) { print 'FATAL: File \'' . $finrrd . '\' is not writable by this account.' . PHP_EOL; exit(-8); } break; case '-R': case '--owner': $owner = $value; $ownerset = true; break; case '-B': case '--backup': $backup = true; break; case '-O': case '--overwrite': $overwrite = true; break; case '-d': case '--debug': $debug = true; break; case '-D': case '--dryrun': $dryrun = true; break; case '-V': case '--version': display_version(); exit(0); case '-H': case '--help': display_help(); exit(0); default: print 'ERROR: Invalid Parameter ' . $parameter . PHP_EOL . PHP_EOL; display_help(); exit(-3); } } } if ($from_cacti) { print 'NOTE: Running from the Cacti Server' . PHP_EOL; } else { print 'NOTE: Not running from the Cacti Server' . PHP_EOL; } // additional error check if ($oldrrd == '') { print 'FATAL: You must specify a old RRDfile!' . PHP_EOL . PHP_EOL; display_help(); exit(-2); } if ($newrrd == '') { print 'FATAL: You must specify a New RRDfile!' . PHP_EOL . PHP_EOL; display_help(); exit(-2); } if ($overwrite && $finrrd == '') { $finrrd = $newrrd; } if ($finrrd == '') { print 'FATAL: You must specify a New RRDfile or use the overwrite option!' . PHP_EOL . PHP_EOL; display_help(); exit(-2); } debug('Entering Mainline'); // let's see if we can find rrdtool global $rrdtool, $use_db, $db; // see if SQLite is available if (class_exists('SQLite3')) { print 'NOTE: Using SQLite Database for performance.' . PHP_EOL; $use_db = true; } else { print 'NOTE: Using Native Arrays due to lack of SQLite.' . PHP_EOL; $use_db = false; } // verify the location of rrdtool if (function_exists('read_config_option')) { $rrdtool = read_config_option('path_rrdtool'); } if (!file_exists($rrdtool)) { if (substr_count(PHP_OS, 'WIN')) { $rrdtool = 'rrdtool.exe'; } else { $rrdtool = 'rrdtool'; } } $response = shell_exec(cacti_escapeshellcmd($rrdtool)); if (strlen((string) $response)) { $response_array = explode(' ', (string) $response); print 'NOTE: Using ' . $response_array[0] . ' Version ' . $response_array[1] . PHP_EOL; } else { print 'FATAL: RRDTool not found in configuration or path.' . PHP_EOL . 'Please insure RRDTool can be found using one of these methods!' . PHP_EOL; exit(1); } /* The dump files and the backups were previously named from the RRD basename * and mt_rand() directly in a world writable directory, and were created by * shell redirection and copy(), both of which follow symlinks. A local user * who guessed a name could have the output written over a file of their * choosing. Everything now goes in one private directory created for this run, * so the names cannot be claimed in advance. */ $tempdir = tempnam(sys_get_temp_dir(), 'cacti_splice_'); if ($tempdir === false || !unlink($tempdir) || !mkdir($tempdir, 0700)) { print 'FATAL: Unable to create a private working directory' . PHP_EOL; exit(1); } $oldxmlfile = $tempdir . '/' . str_replace('.rrd', '', basename($oldrrd)) . '.dump'; $newxmlfile = $tempdir . '/' . str_replace('.rrd', '', basename($newrrd)) . '.dump'; if ($finrrd === '') { $finrrd = dirname($newrrd) . '/' . basename($newrrd) . '.new'; } // execute the dump commands debug("Creating XML file '$oldxmlfile' from '$oldrrd'"); shell_exec(cacti_escapeshellcmd($rrdtool) . ' dump ' . cacti_escapeshellarg($oldrrd) . ' > ' . cacti_escapeshellarg($oldxmlfile)); debug("Creating XML file '$newxmlfile' from '$newrrd'"); shell_exec(cacti_escapeshellcmd($rrdtool) . ' dump ' . cacti_escapeshellarg($newrrd) . ' > ' . cacti_escapeshellarg($newxmlfile)); // read the xml files into arrays if (file_exists($oldxmlfile)) { $old_output = file($oldxmlfile); // remove the temp file unlink($oldxmlfile); } else { print 'FATAL: RRDtool Command Failed on \'' . $oldrrd . '\'. Please insure your RRDtool install is valid!' . PHP_EOL; exit(-12); } if (file_exists($newxmlfile)) { $new_output = file($newxmlfile); // remove the temp file unlink($newxmlfile); } else { print 'FATAL: RRDtool Command Failed on \'' . $newrrd . '\'. Please insure your RRDtool install is valid!' . PHP_EOL; exit(-12); } print 'NOTE: RRDfile will be written to \'' . $finrrd . '\'' . PHP_EOL; // Read the old RRDfile into an array, flatten and remove gaps debug("Reading XML From '$oldrrd'"); $old_output = preProcessXML($old_output); $old_rrd = processXML($old_output); $old_flat = flattenXML($old_rrd); if ($use_db) { $db = createTable(); loadTable($db, $old_flat); } // Read the new RRDfile into an array debug("Reading XML From '$newrrd'"); $new_output = preProcessXML($new_output); $new_rrd = processXML($new_output); // Splice new RRDfiles array with the flattened data debug('Splicing RRDfiles'); spliceRRDs($new_rrd, $old_flat, $old_rrd['dsnames']); debug('Re-Creating XML File'); $new_xml = recreateXML($new_rrd); debug('Writing XML File to Disk'); file_put_contents($newxmlfile, $new_xml); // finally update the file XML file and Reprocess the RRDfile if (!$dryrun) { debug('Creating New RRDfile'); createRRDFileFromXML($newxmlfile, $finrrd); } // remove the temp file unlink($newxmlfile); // change ownership if ($ownerset) { if ($user == 'root') { chown($finrrd, $owner); } else { print 'ERROR: Unable to change owner. You must run as root to change owner' . PHP_EOL; } } memoryUsage(); /** * spliceRRDs - This function walks through the structure of the newrrd * XML file array and for each value, if it's either '0' or 'NaN' the * script will search the flattenedXML or SQLite table for the closest * match and save that into the final array, that will then be written * back out to an XML file and re-loaded into an RRDfile. * * @param array $new_rrd * @param array $old_flat * @param array $old_dsnames * * @return void */ function spliceRRDs(array &$new_rrd, array &$old_flat, array &$old_dsnames) : void { if (cacti_sizeof($new_rrd) && cacti_sizeof($old_flat)) { if (isset($new_rrd['rra'])) { foreach ($new_rrd['rra'] as $rra_num => $rra) { $cf = $new_rrd['rra'][$rra_num]['cf']; $pdp = $new_rrd['rra'][$rra_num]['pdp_per_row']; if (isset($rra['database'])) { foreach ($rra['database'] as $cdp_ds_num => $value) { $dsname = $new_rrd['ds'][$cdp_ds_num]['name']; $olddsnum = $old_dsnames[$dsname]; $last_good = 'NaN'; debug("Splicing DSName $dsname NewId $cdp_ds_num OldId $olddsnum"); foreach ($value as $time => $v) { if ($v == 'NaN' || $v == 0) { if ($time < $old_flat['mintime']) { continue; } $old_value = getOldRRDValue($old_flat, $olddsnum, $cf, $time); if ($old_value != 'NaN') { $last_good = $old_value; $new_rrd['rra'][$rra_num]['database'][$cdp_ds_num][$time] = $old_value; } elseif ($last_good != 'NaN') { $new_rrd['rra'][$rra_num]['database'][$cdp_ds_num][$time] = $last_good; } } else { $last_good = $v; } $new_value = $new_rrd['rra'][$rra_num]['database'][$cdp_ds_num][$time]; // print "DSName: $dsname, NewValue: $new_value, OrigValue: $v, OldValue: $old_value\n"; } } } else { print 'FATAL: RRA database is Invalid' . PHP_EOL; } } } else { print 'FATAL: One of RRA\'s is Invalid' . PHP_EOL; } } else { print 'FATAL: One of RRD\'s is Invalid' . PHP_EOL; } } /** * getOldRRDValue - scan the flattened array for a good timestamp * and return the nearest value for that timestamp. * * The flattened array is sorted by timestamp in reverse order. * If the SQLite table is available, this function will prefer * that table over traversing the array. * * @param array $old_flat * @param int $dsnum * @param string $cf * @param int $time * * @return string - The value from the old RRDfile */ function getOldRRDValue(array &$old_flat, int $dsnum, string $cf, int $time) : string { global $use_db, $db; if ($use_db) { $stmt = $db->prepare("SELECT * FROM dsData WHERE dsid = $dsnum AND cf = '$cf' AND timestamp <= $time ORDER BY timestamp DESC LIMIT 1"); $result = $stmt->execute(); while ($row = $result->fetchArray()) { return $row['value']; } return 'NaN'; } else { if (!isset($old_flat[$dsnum][$cf])) { debug("CF $cf Not found in flattened data."); return 'NaN'; } else { $first = true; foreach ($old_flat[$dsnum][$cf] as $timestamp => $data) { if ($first && $time > $timestamp) { // The time is before any good data in the RRDfile debug("No Good data found. Timestamp $time newer than the newest timestamp $timestamp"); return 'NaN'; } if ($time >= $timestamp) { debug("Good for $time offset is " . number_format(abs($time - $timestamp), 0)); return $data; } $first = false; } debug("No Good data found. Timestamp $time"); return 'NaN'; } } } /** * recreateXML - Take the data from the modified XML and re-create the XML file that * will then be turned back into an RRDfile. * * The array structure is documented below. * * $rrd['version']; * $rrd['step']; * $rrd['lastupdate']; * $rrd['ds'][$ds_num]['name']; * $rrd['ds'][$ds_num]['type']; * $rrd['ds'][$ds_num]['minimal_heartbeat']; * $rrd['ds'][$ds_num]['min']; * $rrd['ds'][$ds_num]['max']; * $rrd['ds'][$ds_num]['last_ds']; * $rrd['ds'][$ds_num]['value']; * $rrd['ds'][$ds_num]['unknown_sec']; * $rrd['rra'][$rra_num]['cf']; * $rrd['rra'][$rra_num]['pdp_per_row']; * $rrd['rra'][$rra_num]['params']['xff']; * $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['primary_value']; * $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['secondary_value']; * $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['value']; * $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['unknown_datapoints']; * $rrd['rra'][$rra_num]['database'][$cdp_ds_num]['time']; * * @param array $new_rrd - The Array structure of the new RRDfile * * @return string - The new RRDfile output */ function recreateXML(array $new_rrd) : string { $rrd = "\n"; $rrd .= "\t " . $new_rrd['version'] . " \n"; $rrd .= "\t " . $new_rrd['step'] . " \n"; $rrd .= "\t " . $new_rrd['lastupdate'] . " \n"; foreach ($new_rrd['ds'] as $dsnum => $ds) { $rrd .= "\t\n"; $rrd .= "\t\t " . $ds['name'] . " \n"; $rrd .= "\t\t " . $ds['type'] . " \n"; $rrd .= "\t\t " . $ds['minimal_heartbeat'] . " \n"; $rrd .= "\t\t " . $ds['min'] . " \n"; $rrd .= "\t\t " . $ds['max'] . " \n"; $rrd .= "\t\t " . $ds['last_ds'] . " \n"; $rrd .= "\t\t " . $ds['value'] . " \n"; $rrd .= "\t\t " . $ds['unknown_sec'] . " \n"; $rrd .= "\t\n"; } foreach ($new_rrd['rra'] as $rra_num => $rra) { $output = []; $rrd .= "\t\n"; $rrd .= "\t\t " . $rra['cf'] . " \n"; $rrd .= "\t\t " . $rra['pdp_per_row'] . " \n"; $rrd .= "\t\t\n"; $rrd .= "\t\t\t " . $rra['params']['xff'] . " \n"; $rrd .= "\t\t\n"; $rrd .= "\t\t\n"; foreach ($new_rrd['rra'][$rra_num]['cdp_prep'] as $cdp_ds_num => $pdp) { $rrd .= "\t\t\t\n"; $rrd .= "\t\t\t\t " . $pdp['primary_value'] . " \n"; $rrd .= "\t\t\t\t " . $pdp['secondary_value'] . " \n"; $rrd .= "\t\t\t\t " . $pdp['value'] . " \n"; $rrd .= "\t\t\t\t " . $pdp['unknown_datapoints'] . " \n"; $rrd .= "\t\t\t\n"; foreach ($new_rrd['rra'][$rra_num]['database'] as $dsnum => $v) { foreach ($v as $time => $value) { $output[$time][$dsnum] = $value; } } } $rrd .= "\t\t\n"; $rrd .= "\t\t\n"; foreach ($output as $time => $v) { $rrd .= "\t\t\t"; foreach ($v as $dsnum => $value) { $rrd .= ' ' . $value . ' '; } $rrd .= "\n"; } $rrd .= "\t\t\n"; $rrd .= "\t\n"; } $rrd .= ''; return $rrd; } /** * memoryUsage - Report the peak memory usage of the php script * * @return void */ function memoryUsage() : void { global $time; $mem_usage = memory_get_usage(true); if ($mem_usage < 1024) { $memstr = $mem_usage . ' B'; } elseif ($mem_usage < 1048576) { $memstr = round($mem_usage / 1024, 2) . ' KB'; } else { $memstr = round($mem_usage / 1048576, 2) . ' MB'; } print 'NOTE: Time:' . round(microtime(true) - $time, 2) . ', RUsage:' . $memstr . PHP_EOL; } /** * flattenXML - Take all the data from the various data sources and * by Consolidation Function, sort the values by timestamp so that * the new RRDfile can pull values that make sense to fill in the * time where there may be no data. * * Additionally, remove any NaN values and replace with the last * good known value to fill gaps in the graphs. * * The form of the output array will be as follows: * * $newxml[$datasourceid][$cf][$timestamp] = value * $newxml['mintime'] = value * * The data will only go back as far as the source RRDfile. * * @param array $xml - The xml data to be flattened * * @return array - The flattened XML file */ function flattenXML(array &$xml) : array { global $debug; $newxml = []; $maxarray = []; $mintime = 'NaN'; if (cacti_sizeof($xml['rra'])) { foreach ($xml['rra'] as $rraid => $data) { $cf = $data['cf']; foreach ($data['database'] as $dsid => $timedata) { $i = 0; // Don't load data from less granular RRA's if (isset($maxarray[$dsid][$cf])) { $maxtoload = $maxarray[$dsid][$cf]; } else { $maxtoload = 9999999999999; } foreach ($timedata as $timestamp => $value) { if ($i == 0 && $value != 'NaN') { $maxarray[$dsid][$cf] = $timestamp; $i++; } if ($timestamp <= $maxtoload) { $newxml[$dsid][$cf][$timestamp] = $value; if ($mintime == 'NaN') { $mintime = $timestamp; } elseif ($timestamp < $mintime) { $mintime = $timestamp; } } } } } foreach ($newxml as $dsid => $data) { foreach ($data as $cf => $timedata) { // Sort the data ksort($timedata); // Get rid of NaN $last_data = 0; foreach ($timedata as $timestamp => $data) { if ($data == 'NaN') { $timedata[$timestamp] = $last_data; } else { $last_data = $data; } } krsort($timedata); $newxml[$dsid][$cf] = $timedata; if ($debug) { $stats = sprintf( 'DS:%2d, CF:%8s, Vals:%10s, Max:%10s, Avg:%10s', $dsid, $cf, number_format(cacti_sizeof($timedata)), number_format(getMaxValue($timedata), 4), number_format(getAvgValue($timedata), 4) ); debug($stats); } } } } $newxml['mintime'] = $mintime; return $newxml; } /** * getMaxValue - Obtains the max value from the timestamp array * for use in debug output. * * @param array $data - An array of objects to get the max from * * @return float - The maximum value */ function getMaxValue(array &$data) : float { $max = 0; foreach ($data as $timestamp => $value) { if ($value != 'NaN' && $value > $max) { $max = $value; } } return $max; } /** * getAvgValue - Obtains the average value from the timestamp array * for use in debug output. * * @param array $data - An array of objects to get the average from * * @return float - The average value */ function getAvgValue(mixed &$data) : float { $entries = cacti_sizeof($data); $total = array_sum($data); if ($entries) { return $total / $entries; } else { return 0; } } /** * processXML - Read all the XML into an array. The format of the array * will be as show below. This way it can be processed reverted back * to array format at the end of the merge process. * * $rrd['version']; * $rrd['step']; * $rrd['lastupdate']; * $rrd['ds'][$ds_num]['name']; * $rrd['ds'][$ds_num]['type']; * $rrd['ds'][$ds_num]['minimal_heartbeat']; * $rrd['ds'][$ds_num]['min']; * $rrd['ds'][$ds_num]['max']; * $rrd['ds'][$ds_num]['last_ds']; * $rrd['ds'][$ds_num]['value']; * $rrd['ds'][$ds_num]['unknown_sec']; * $rrd['rra'][$rra_num]['cf']; * $rrd['rra'][$rra_num]['pdp_per_row']; * $rrd['rra'][$rra_num]['params']['xff']; * $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['primary_value']; * $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['secondary_value']; * $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['value']; * $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['unknown_datapoints']; * $rrd['rra'][$rra_num]['database'][$cdp_ds_num]['time']; * * @param array $output * * @return array - The processed XML data */ function processXML(array &$output) : array { $rrd = []; $dsnames = []; $rra_num = 0; $ds_num = 0; $cdp_ds_num = 0; $in_ds = false; $in_rra = false; $in_db = false; $in_parm = false; $in_cdp = false; $in_cdp_ds = false; if (cacti_sizeof($output)) { foreach ($output as $line) { if (substr_count($line, '')) { $line = trim(str_replace('', '', str_replace('', '', (string) $line))); $larray = explode('', $line); $time = trim(str_replace('', '', str_replace('', '', $larray[0]))); array_shift($larray); $tdsno = 0; foreach ($larray as $l) { $value = trim(str_replace('', '', $l)); $rrd['rra'][$rra_num]['database'][$tdsno][$time] = $value; $tdsno++; } } elseif (substr_count($line, '')) { $rrd['lastupdate'] = XMLrip('lastupdate', $line); } elseif (substr_count($line, '')) { $line = trim(str_replace('', '', $line)); $rrd['version'] = XMLrip('version', $line); } elseif (substr_count($line, '')) { $rrd['step'] = XMLrip('step', $line); } elseif (substr_count($line, '')) { $in_rra = true; } elseif (substr_count($line, '')) { $in_rra = false; $cdp_ds_num = 0; $rra_num++; } elseif (substr_count($line, '')) { if (!$in_cdp) { $in_ds = true; } } elseif (substr_count($line, '')) { if ($in_ds) { $in_ds = false; $ds_num++; } else { $in_cdp_ds = false; $cdp_ds_num++; } } elseif (substr_count($line, '')) { $in_cdp = true; } elseif (substr_count($line, '')) { $in_cdp = false; } elseif (substr_count($line, '')) { $in_parm = true; } elseif (substr_count($line, '')) { $in_parm = false; } elseif (substr_count($line, '')) { $rrd['ds'][$ds_num]['name'] = XMLrip('name', $line); $dsnames[] = XMLrip('name', $line); } elseif (substr_count($line, '')) { $rrd['ds'][$ds_num]['type'] = XMLrip('type', $line); } elseif (substr_count($line, '')) { $rrd['ds'][$ds_num]['minimal_heartbeat'] = XMLrip('minimal_heartbeat', $line); } elseif (substr_count($line, '')) { $rrd['ds'][$ds_num]['max'] = XMLrip('max', $line); } elseif (substr_count($line, '')) { $rrd['ds'][$ds_num]['min'] = XMLrip('min', $line); } elseif (substr_count($line, '')) { $rrd['ds'][$ds_num]['last_ds'] = XMLrip('last_ds', $line); } elseif (substr_count($line, '')) { if ($in_rra) { $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['value'] = XMLrip('value', $line); } else { $rrd['ds'][$ds_num]['value'] = XMLrip('value', $line); } } elseif (substr_count($line, '')) { $rrd['ds'][$ds_num]['unknown_sec'] = XMLrip('unknown_sec', $line); } elseif (substr_count($line, '')) { $rrd['rra'][$rra_num]['cf'] = XMLrip('cf', $line); } elseif (substr_count($line, '')) { $rrd['rra'][$rra_num]['pdp_per_row'] = XMLrip('pdp_per_row', $line); } elseif (substr_count($line, '')) { $rrd['rra'][$rra_num]['params']['xff'] = XMLrip('xff', $line); } elseif (substr_count($line, '')) { $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['primary_value'] = XMLrip('primary_value', $line); } elseif (substr_count($line, '')) { $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['secondary_value'] = XMLrip('secondary_value', $line); } elseif (substr_count($line, '')) { $rrd['rra'][$rra_num]['cdp_prep'][$cdp_ds_num]['unknown_datapoints'] = XMLrip('unknown_datapoints', $line); } } } if (cacti_sizeof($dsnames)) { foreach ($dsnames as $index => $name) { $rrd['dsnames'][$name] = $index; } } return $rrd; } // All Functions function createRRDFileFromXML(string $xmlfile, string $rrdfile) : void { global $rrdtool; // execute the dump command print 'NOTE: Re-Importing \'' . $xmlfile . '\' to \'' . $rrdfile . '\'' . PHP_EOL; $return_code = 0; $output = []; $command = "$rrdtool restore -f -r $xmlfile $rrdfile"; $result = exec($command, $output, $return_var); if ($return_var == 0) { print "NOTE: File $rrdfile Restored Correctly" . PHP_EOL; } else { print "WARNING: File $rrdfile Encountered Errors. Errors below:" . PHP_EOL; foreach ($output as $l) { print "WARNING: $l" . PHP_EOL; } } } function XMLrip(string $tag, string $line) : string { return trim(str_replace("<$tag>", '', str_replace("", '', $line))); } function writeXMLFile(string $output, string $xmlfile) : mixed { return file_put_contents($xmlfile, $output); } function backupRRDFile(string $rrdfile) : bool { global $tempdir, $html; $backupdir = $tempdir; /* the working directory is private to this run, so a name only has to be * unique within it rather than unguessable */ $newfile = basename($rrdfile); for ($i = 1; file_exists($backupdir . '/' . $newfile); $i++) { $newfile = basename($rrdfile) . '.' . $i; } print 'NOTE: Backing Up \'' . $rrdfile . '\' to \'' . $backupdir . '/' . $newfile . '\'' . PHP_EOL; return copy($rrdfile, $backupdir . '/' . $newfile); } /** * preProcessXML - This function strips the timestamps off the XML dump * and loads that data into an array along with the remainder of the * XML data for future processing. * * @param array $output - The output from the RRDtool command * * @return array - The updated XML object */ function preProcessXML(array &$output) : array { $new_array = []; if (cacti_sizeof($output)) { foreach ($output as $line) { $line = trim($line); $date = ''; if ($line == '') { continue; } else { // is there a comment, remove it $comment_start = strpos($line, ''); $row = strpos($line, ''); if ($row > 0) { $date = trim(substr($line,strpos($line,'/') + 1,11)); } if ($comment_start == 0) { $line = trim(substr($line, $comment_end + 3)); } else { $line = trim(substr($line, 0, $comment_start - 1) . substr($line, $comment_end + 3)); } if (!empty($date)) { $line = str_replace('', " $date ", $line); } } if ($line != '') { $new_array[] = $line; } } } } // transfer the new array back to the original array return $new_array; } /** * Simple generic debug function * * @return void */ function debug(string $string) : void { global $debug; if ($debug) { print 'DEBUG: ' . trim($string) . PHP_EOL; } } /** * createTable - This function creates a SQLite memory table * to hold the flattened XML file in for replay. * * @return object - The SQLite memory database object */ function createTable() : object { // table in memory $db = new SQLite3(':memory:'); // create the table $db->exec('CREATE TABLE dsData ( dsid int, cf char(10) NOT NULL, timestamp int, value real NOT NULL, PRIMARY KEY (dsid, cf, timestamp))'); $db->exec('CREATE INDEX dsid_cf_timestamp ON dsData (dsid, cf, timestamp)'); $db->exec('CREATE INDEX timestamp ON dsData (timestamp)'); return $db; } /** * loadTable - This function loads the flattened XML file into * the SQLite database for replaying the RRDfile dump data * into the new XML file. * * @param object $db * @param array $records * * @return void */ function loadTable(object $db, array &$records) : void { $db->exec('BEGIN TRANSACTION'); $sql = ''; foreach ($records as $dsid => $cfdata) { if (is_numeric($dsid)) { foreach ($cfdata as $cf => $timedata) { $i = 0; foreach ($timedata as $timestamp => $value) { $sql .= ($sql != '' ? ', ' : '') . '(' . $dsid . ',"' . $cf . '",' . $timestamp . ', ' . $value . ')'; $i++; if ($i > 50) { if ($sql != '') { $db->exec("INSERT INTO dsData (dsid, cf, timestamp, value) VALUES $sql"); } $sql = ''; $i = 0; } } if ($sql != '') { $db->exec("INSERT INTO dsData (dsid, cf, timestamp, value) VALUES $sql"); } $sql = ''; } } } $db->exec('COMMIT TRANSACTION'); } /** * display_version - displays version information * * @return void */ function display_version() : void { if (!defined('COPYRIGHT_YEARS')) { define('COPYRIGHT_YEARS', '2004-2026'); } $version = get_cacti_cli_version(); print 'Cacti RRDfile Splicer Utility, Version ' . $version . ', ' . COPYRIGHT_YEARS . PHP_EOL; } /** * display_help - displays help information * * @return void */ function display_help() : void { display_version(); print PHP_EOL . 'usage: splice_rrd.php --oldrrd=file --newrrd=file [--finrrd=file]' . PHP_EOL; print ' [--owner=apache] [--debug] [--dryrun]' . PHP_EOL . PHP_EOL; print 'The splice_rrd.php file is designed to allow two RRDfiles to be merged.' . PHP_EOL . PHP_EOL; print 'This utility can effectively change the resolution/step of an RRDfile' . PHP_EOL; print 'so long as the new RRDfile already has the correct step.' . PHP_EOL . PHP_EOL; print 'The Old and New input parameters are mandatory. If the finrrd option is' . PHP_EOL; print 'not specified, it will be the newrrd plus a timestamp.' . PHP_EOL . PHP_EOL; print 'Required:' . PHP_EOL; print '--oldrrd=file - The old RRDfile that contains old data.' . PHP_EOL; print '--newrrd=file - The new RRDfile that contains more recent data.' . PHP_EOL . PHP_EOL; print 'Optional:' . PHP_EOL; print '--finrrd=file - The final RRDfile that contains data from both rrdfiles.' . PHP_EOL; print '--owner=apache - Change the owner of the resulting file. Note requires root.' . PHP_EOL; print '--dryrun - Simply test the splicing of the RRDfiles, don\'t write.' . PHP_EOL . PHP_EOL; }