Skip to content

Commit 74745d2

Browse files
murrantStyleCIBot
andauthored
Rrdtool use RrdProcess (librenms#18786)
* Rrdtool use RrdProcess * Apply fixes from StyleCI * use stop method * Run all commands the same * clear output before running command * correct spot * Catch RrdNotFoundException in checkRrdExists method * Fix bad attempted escaping * Handle update too frequent exception * Remove duplicated command print And don't print RRDtool Output: if there is no output --------- Co-authored-by: StyleCI Bot <bot@styleci.io>
1 parent 2263de5 commit 74745d2

6 files changed

Lines changed: 83 additions & 480 deletions

File tree

LibreNMS/Data/Store/Rrd.php

Lines changed: 51 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,23 @@
3131
use App\Models\Eventlog;
3232
use App\Polling\Measure\Measurement;
3333
use Exception;
34+
use Illuminate\Support\Facades\Log;
3435
use Illuminate\Support\Str;
35-
use LibreNMS\Enum\ImageFormat;
3636
use LibreNMS\Enum\Severity;
3737
use LibreNMS\Exceptions\FileExistsException;
3838
use LibreNMS\Exceptions\RrdException;
3939
use LibreNMS\Exceptions\RrdGraphException;
40-
use LibreNMS\Proc;
40+
use LibreNMS\Exceptions\RrdNotFoundException;
41+
use LibreNMS\Exceptions\RrdUpdateTooFrequentException;
4142
use LibreNMS\RRD\RrdProcess;
4243
use LibreNMS\Util\Debug;
4344
use LibreNMS\Util\Rewrite;
44-
use Log;
45-
use Symfony\Component\Process\Process;
4645

4746
class Rrd extends BaseDatastore
4847
{
4948
private $disabled = false;
5049

51-
private ?Proc $sync_process = null;
52-
private ?Proc $async_process = null;
50+
private ?RrdProcess $rrd = null;
5351
/** @var string */
5452
private $rrd_dir;
5553
/** @var string */
@@ -60,8 +58,6 @@ class Rrd extends BaseDatastore
6058
private array $rra;
6159
/** @var int */
6260
private $step;
63-
/** @var string */
64-
private $rrdtool_executable;
6561

6662
public function __construct()
6763
{
@@ -92,67 +88,20 @@ protected function loadConfig(): void
9288
' RRA:LAST:0.5:1:2016 '
9389
)));
9490
$this->version = LibrenmsConfig::get('rrdtool_version', '1.4');
95-
$this->rrdtool_executable = LibrenmsConfig::get('rrdtool', 'rrdtool');
96-
}
97-
98-
/**
99-
* Opens up a pipe to RRDTool using handles provided
100-
*
101-
* @param bool $dual_process start an additional process that's output should be read after every command
102-
* @return bool the process(s) have been successfully started
103-
*/
104-
public function init(bool $dual_process = true): bool
105-
{
106-
$command = $this->rrdtool_executable . ' -';
107-
108-
$descriptor_spec = [
109-
0 => ['pipe', 'r'], // stdin is a pipe that the child will read from
110-
1 => ['pipe', 'w'], // stdout is a pipe that the child will write to
111-
2 => ['pipe', 'w'], // stderr is a pipe that the child will write to
112-
];
113-
114-
$cwd = $this->rrd_dir;
115-
116-
try {
117-
if (! $this->isSyncRunning()) {
118-
$this->sync_process = new Proc($command, $descriptor_spec, $cwd);
119-
}
120-
121-
if ($dual_process && ! $this->isAsyncRunning()) {
122-
$this->async_process = new Proc($command, $descriptor_spec, $cwd);
123-
$this->async_process->setSynchronous(false);
124-
}
125-
126-
return $this->isSyncRunning() && ($dual_process ? $this->isAsyncRunning() : true);
127-
} catch (Exception $e) {
128-
Log::error('Failed to start RRD datastore: ' . $e->getMessage());
129-
130-
return false;
131-
}
13291
}
13392

134-
public function isSyncRunning(): bool
93+
public function init(int $timeout = 600): void
13594
{
136-
return isset($this->sync_process) && $this->sync_process->isRunning();
137-
}
138-
139-
public function isAsyncRunning(): bool
140-
{
141-
return isset($this->async_process) && $this->async_process->isRunning();
95+
$this->rrd ??= app(RrdProcess::class, ['timeout' => $timeout]);
14296
}
14397

14498
/**
145-
* Close all open rrdtool processes.
99+
* Close rrdtool process.
146100
* This should be done before exiting
147101
*/
148102
public function terminate(): void
149103
{
150-
if ($this->isSyncRunning()) {
151-
$this->sync_process->close('quit');
152-
}
153-
if ($this->isAsyncRunning()) {
154-
$this->async_process->close('quit');
155-
}
104+
$this->rrd?->stop();
156105
}
157106

158107
/**
@@ -187,21 +136,25 @@ public function write(string $measurement, array $fields, array $tags = [], arra
187136

188137
return $valid;
189138
}, ARRAY_FILTER_USE_KEY);
139+
}
190140

191-
if (! $this->checkRrdExists($rrd)) {
192-
$options = ['--step', $step, ...$rrd_def->getArguments(), ...$this->rra];
193-
$this->command('create', $rrd, $options);
141+
try {
142+
$this->update($rrd, $fields);
143+
} catch (RrdUpdateTooFrequentException) {
144+
Log::debug("RRD warning: update too soon for $rrd");
145+
} catch (RrdNotFoundException) {
146+
if (isset($rrd_def)) {
147+
$this->command('create', $rrd, ['--step', $step, ...$rrd_def->getArguments(), ...$this->rra]);
148+
$this->update($rrd, $fields);
194149
}
195150
}
196-
197-
$this->update($rrd, $fields);
198151
}
199152

200153
public function lastUpdate(string $filename): ?TimeSeriesPoint
201154
{
202-
$output = $this->command('lastupdate', $filename)[0];
155+
$output = $this->command('lastupdate', $filename);
203156

204-
if (preg_match('/((?: \w+)+)\n\n(\d+):((?: [\d.-]+)+)\nOK/', (string) $output, $matches)) {
157+
if (preg_match('/((?: \w+)+)\n\n(\d+):((?: [\d.-]+)+)\nOK/', $output, $matches)) {
205158
$data = array_combine(
206159
explode(' ', ltrim($matches[1])),
207160
explode(' ', ltrim($matches[3])),
@@ -219,33 +172,17 @@ public function lastUpdate(string $filename): ?TimeSeriesPoint
219172
*
220173
* @param string $filename
221174
* @param array $data
222-
* @return array
223175
*
224176
* @throws RrdException
177+
* @throws Exception
225178
*
226179
* @internal
227180
*/
228-
public function update($filename, $data): array
181+
public function update(string $filename, array $data): void
229182
{
230-
$values = [];
231-
// Do some sanitation on the data if passed as an array.
232-
233-
if (is_array($data)) {
234-
$values[] = 'N';
235-
foreach ($data as $v) {
236-
if (! is_numeric($v)) {
237-
$v = 'U';
238-
}
239-
240-
$values[] = $v;
241-
}
183+
$data = 'N:' . implode(':', array_map(fn ($v) => is_numeric($v) ? $v : 'U', $data));
242184

243-
$data = implode(':', $values);
244-
245-
return $this->command('update', $filename, [$data]);
246-
}
247-
248-
throw new RrdException('Bad options passed to rrdtool_update');
185+
$this->command('update', $filename, [$data]);
249186
}
250187

251188
// rrdtool_update
@@ -379,7 +316,7 @@ public function name($host, $extra, $extension = '.rrd'): string
379316
*/
380317
public function dirFromHost($host): string
381318
{
382-
$host = self::safeName(trim($host, '[]'));
319+
$host = self::safeName(trim((string) $host, '[]'));
383320

384321
return Str::finish($this->rrd_dir, '/') . $host;
385322
}
@@ -392,24 +329,24 @@ public function dirFromHost($host): string
392329
* @param string $command create, update, updatev, graph, graphv, dump, restore, fetch, tune, first, last, lastupdate, info, resize, xport, flushcached
393330
* @param string $filename The full patth to the rrd file
394331
* @param array $options rrdtool command options
395-
* @return array the output of stdout and stderr in an array
332+
* @return string the output of the command
396333
*
397334
* @throws Exception thrown when the rrdtool process(s) cannot be started
398335
*/
399-
private function command(string $command, string $filename, array $options = []): array
336+
private function command(string $command, string $filename, array $options = []): string
400337
{
401338
$stat = Measurement::start($this->coalesceStatisticType($command));
402-
$output = null;
339+
$output = '';
403340

404341
try {
405342
$cmd = self::buildCommand($command, $filename, $options);
406343
} catch (FileExistsException) {
407344
Log::debug("RRD[%g$filename already exists%n]", ['color' => true]);
408345

409-
return [null, null];
346+
return $output;
410347
}
411348

412-
Log::debug('RRD[%g' . implode(' ', $cmd) . '%n]', ['color' => true]);
349+
$commandLine = implode(' ', $cmd);
413350

414351
// do not write rrd files, but allow read-only commands
415352
$ro_commands = ['graph', 'graphv', 'dump', 'fetch', 'first', 'last', 'lastupdate', 'info', 'xport'];
@@ -418,24 +355,14 @@ private function command(string $command, string $filename, array $options = [])
418355
Log::debug('[%rRRD Disabled%n]', ['color' => true]);
419356
}
420357

421-
return [null, null];
358+
return $output;
422359
}
423360

424-
// send the command!
425-
if (in_array($command, ['last', 'list', 'lastupdate']) && $this->init(false)) {
426-
// send this to our synchronous process so output is guaranteed
427-
$output = $this->sync_process->sendCommand(implode(' ', $cmd));
428-
} elseif ($this->init()) {
429-
// don't care about the return of other commands, so send them to the faster async process
430-
$output = $this->async_process->sendCommand(implode(' ', $cmd));
431-
} else {
432-
Log::error('rrdtool could not start');
433-
}
361+
$this->init();
362+
$output = $this->rrd->run($commandLine);
434363

435-
if (Debug::isVerbose()) {
436-
echo 'RRDtool Output: ';
437-
echo $output[0];
438-
echo $output[1];
364+
if (Debug::isVerbose() && $output) {
365+
Log::debug('RRDtool Output: ' . $output);
439366
}
440367

441368
$this->recordStatistic($stat->end());
@@ -493,8 +420,7 @@ public function getRrdFiles(string $hostname): array
493420
{
494421
if ($this->rrdcached) {
495422
$output = $this->command('list', '/' . self::safeName($hostname));
496-
497-
$files = explode("\n", trim($output[0] ?? ''));
423+
$files = explode("\n", trim($output));
498424
array_pop($files); // remove rrdcached status line
499425
} else {
500426
$files = glob($this->dirFromHost($hostname) . '/*.rrd') ?: [];
@@ -552,10 +478,14 @@ public function getRrdApplicationArrays($device, $app_id, $app_name, $category =
552478
public function checkRrdExists($filename): bool
553479
{
554480
if ($this->rrdcached && version_compare($this->version, '1.5', '>=')) {
555-
$check_output = implode('', $this->command('last', $filename));
556-
$filename = str_replace([$this->rrd_dir . '/', $this->rrd_dir], '', $filename);
481+
try {
482+
$check_output = $this->command('last', $filename);
483+
$filename = str_replace([$this->rrd_dir . '/', $this->rrd_dir], '', $filename);
557484

558-
return ! (str_contains($check_output, $filename) && str_contains($check_output, 'No such file or directory'));
485+
return ! (str_contains($check_output, $filename) && str_contains($check_output, 'No such file or directory'));
486+
} catch (RrdNotFoundException) {
487+
return false;
488+
}
559489
} else {
560490
return is_file($filename);
561491
}
@@ -585,52 +515,23 @@ public function purge($hostname, $prefix): void
585515
* Graphs are a single command per run, so this just runs rrdtool
586516
*
587517
* @param array $options
588-
* @param array|null $env
589518
* @return string
590519
*
591520
* @throws RrdGraphException
592521
*/
593-
public function graph(array $options, ?array $env = null): string
522+
public function graph(array $options): string
594523
{
595-
$process = new Process([$this->rrdtool_executable, '-'], $this->rrd_dir, $env);
596-
$process->setTimeout(300);
597-
$process->setIdleTimeout(300);
598-
599524
try {
600525
$command = $this->buildCommand('graph', '-', $options);
601-
$process->setInput('"' . implode('" "', $command) . "\"\nquit");
602-
$process->run();
603-
} catch (FileExistsException $e) {
604-
throw new RrdGraphException($e->getMessage(), 'File Exists');
605-
}
606526

607-
$feedback_position = strrpos($process->getOutput(), 'OK ');
608-
if ($feedback_position !== false) {
609-
return substr($process->getOutput(), 0, $feedback_position);
610-
}
527+
$this->init(300);
528+
$image = $this->rrd->run('"' . implode('" "', $command) . '"');
529+
$this->rrd->stop();
611530

612-
// if valid image is returned with error, extract image and feedback
613-
// rrdtool defaults to png if imgformat not specified
614-
$imgformat_option = array_find($options, fn ($o) => str_starts_with((string) $o, '--imgformat='));
615-
$graph_type = $imgformat_option ? strtolower(substr((string) $imgformat_option, 12)) : 'png';
616-
$imageFormat = ImageFormat::forGraph($graph_type);
617-
618-
$search = $imageFormat->getImageEnd();
619-
if (($position = strrpos($process->getOutput(), $search)) !== false) {
620-
$position += strlen($search);
621-
throw new RrdGraphException(
622-
substr($process->getOutput(), $position),
623-
null,
624-
null,
625-
null,
626-
$process->getExitCode(),
627-
substr($process->getOutput(), 0, $position)
628-
);
531+
return $image;
532+
} catch (RrdException $e) {
533+
throw new RrdGraphException($e->getMessage(), 'Error');
629534
}
630-
631-
// only error text was returned
632-
$error = trim($process->getOutput() . PHP_EOL . $process->getErrorOutput());
633-
throw new RrdGraphException($error, null, null, null, $process->getExitCode());
634535
}
635536

636537
/**
@@ -690,7 +591,7 @@ public static function fixedSafeDescr($descr, $length): string
690591
*
691592
* @return string
692593
*/
693-
public static function version(): ?string
594+
public static function version(): string
694595
{
695596
try {
696597
$rrd = app(RrdProcess::class, ['timeout' => 10]);
@@ -704,7 +605,7 @@ public static function version(): ?string
704605
//
705606
}
706607

707-
return null;
608+
return '';
708609
}
709610

710611
/**
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace LibreNMS\Exceptions;
4+
5+
class RrdNotFoundException extends RrdException
6+
{
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace LibreNMS\Exceptions;
4+
5+
class RrdUpdateTooFrequentException extends RrdException
6+
{
7+
}

0 commit comments

Comments
 (0)