Skip to content

Commit dd45ed8

Browse files
committed
maintenance:rrd-step update to work with rrdcached
No longer uses a temp file or sed Use RrdProcess and Rrd::getRrdFiles() (fix inconsistencies in getRrdFiles)
1 parent adfbf68 commit dd45ed8

File tree

3 files changed

+89
-62
lines changed

3 files changed

+89
-62
lines changed

LibreNMS/Data/Store/Rrd.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -485,26 +485,29 @@ public function buildCommand(string $command, string $filename, array $options =
485485
}
486486

487487
/**
488-
* Get array of all rrd files for a device,
489-
* via rrdached or localdisk.
488+
* Get array of all rrd files for a device via rrdached or localdisk.
489+
* If $hostname is empty, it will list all rrd files
490490
*
491491
* @param string $hostname hostname of the device
492-
* @return string[] array of rrd files for this host
492+
* @return string[] array of rrd files for this host relative to rrd_dir
493493
*/
494494
public function getRrdFiles(string $hostname): array
495495
{
496-
if ($this->rrdcached) {
497-
$output = $this->command('list', '/' . self::safeName($hostname));
496+
$dir = $this->dirFromHost($hostname);
498497

498+
if ($this->rrdcached) {
499+
$output = $this->command('list', $dir . '/', ['--recursive']);
499500
$files = explode("\n", trim($output[0] ?? ''));
500501
array_pop($files); // remove rrdcached status line
501-
} else {
502-
$files = glob($this->dirFromHost($hostname) . '/*.rrd') ?: [];
502+
$prepend = str_replace($this->rrd_dir . '/', '', $dir) . '/';
503+
504+
return array_map(fn ($file) => $prepend . $file, $files);
503505
}
504506

505-
sort($files);
507+
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir));
508+
$rrdFiles = new \RegexIterator($iterator, '/\.rrd$/');
506509

507-
return $files;
510+
return array_map(fn (\SplFileInfo $file) => str_replace($this->rrd_dir . '/', '', $file), iterator_to_array($rrdFiles, false));
508511
}
509512

510513
/**
@@ -522,6 +525,8 @@ public function getRrdApplicationArrays($device, $app_id, $app_name, $category =
522525
$separator = '-';
523526

524527
$rrdfile_array = $this->getRrdFiles($device['hostname']);
528+
sort($rrdfile_array);
529+
525530
if ($category) {
526531
$pattern = sprintf('%s-%s-%s-%s', 'app', $app_name, $app_id, $category);
527532
} else {

LibreNMS/RRD/RrdProcess.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,13 @@ public function run(string $command, string $waitFor = self::COMMAND_COMPLETE):
7777
return str_contains($buffer, $waitFor);
7878
});
7979

80-
return rtrim($this->process->getOutput());
80+
$output = $this->process->getOutput();
81+
82+
if ($waitFor === self::COMMAND_COMPLETE) {
83+
$output = substr($output, 0, strrpos($output, $waitFor)); // remove OK line
84+
}
85+
86+
return rtrim($output);
8187
}
8288

8389
public function runAsync(string $command): void

app/Console/Commands/MaintenanceRrdStep.php

Lines changed: 68 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,38 @@
66
use App\Facades\DeviceCache;
77
use App\Facades\LibrenmsConfig;
88
use App\Facades\Rrd;
9-
use Illuminate\Support\Str;
9+
use Exception;
10+
use LibreNMS\Exceptions\RrdException;
11+
use LibreNMS\RRD\RrdProcess;
1012
use Symfony\Component\Console\Input\InputArgument;
1113
use Symfony\Component\Console\Input\InputOption;
1214

1315
class MaintenanceRrdStep extends LnmsCommand
1416
{
1517
protected $name = 'maintenance:rrd-step';
1618

19+
private int $systemStep;
20+
private int $icmpStep;
21+
private int $systemHeartbeat;
22+
1723
public function __construct()
1824
{
1925
parent::__construct();
2026
$this->addArgument('device', InputArgument::REQUIRED);
2127
$this->addOption('confirm', null, InputOption::VALUE_NONE);
2228
}
2329

24-
public function handle(): int
30+
public function handle(RrdProcess $rrdProcess): int
2531
{
2632
$this->configureOutputOptions();
2733

34+
$this->systemStep = (int) LibrenmsConfig::get('rrd.step', 300);
35+
$this->icmpStep = (int) LibrenmsConfig::get('ping_rrd_step', $this->systemStep);
36+
$this->systemHeartbeat = (int) LibrenmsConfig::get('rrd.heartbeat', $this->systemStep * 2);
37+
2838
$hostname = (string) $this->argument('device');
2939
if ($hostname !== 'all') {
30-
$hostname = DeviceCache::get($hostname)->hostname;
40+
$hostname = DeviceCache::get($hostname)->hostname; // hostname may be numeric
3141
}
3242

3343
if (empty($hostname)) {
@@ -40,76 +50,82 @@ public function handle(): int
4050
return 0;
4151
}
4252

43-
$systemStep = (int) LibrenmsConfig::get('rrd.step', 300);
44-
$icmpStep = (int) LibrenmsConfig::get('ping_rrd_step', $systemStep);
45-
$systemHeartbeat = (int) LibrenmsConfig::get('rrd.heartbeat', $systemStep * 2);
46-
$rrdtool = (string) LibrenmsConfig::get('rrdtool', 'rrdtool');
47-
$daemon = LibrenmsConfig::get('rrdcached') ? '-d ' . escapeshellarg(LibrenmsConfig::get('rrdcached')) : '';
48-
$tmpPath = (string) LibrenmsConfig::get('temp_dir', '/tmp');
49-
$rrdDir = (string) LibrenmsConfig::get('rrd_dir', LibrenmsConfig::get('install_dir') . '/rrd');
50-
51-
$files = glob($hostname === 'all'
52-
? Str::finish($rrdDir, '/') . '*/*.rrd'
53-
: Rrd::dirFromHost($hostname) . '/*.rrd') ?: [];
53+
$files = Rrd::getRrdFiles($hostname === 'all' ? '' : $hostname);
5454

5555
[$converted, $skipped, $failed] = [0, 0, 0];
5656

5757
foreach ($files as $file) {
58-
$random = rtrim($tmpPath, '/') . '/' . mt_rand() . '.xml';
5958
$rrdFile = basename($file, '.rrd');
6059

6160
[$step, $heartbeat] = $rrdFile === 'icmp-perf'
62-
? [$icmpStep, $icmpStep * 2]
63-
: [$systemStep, $systemHeartbeat];
61+
? [$this->icmpStep, $this->icmpStep * 2]
62+
: [$this->systemStep, $this->systemHeartbeat];
6463

65-
$rrdInfo = shell_exec("$rrdtool info $daemon " . escapeshellarg($file)) ?? '';
64+
try {
65+
$this->checkRrdFile($rrdProcess, $file, $step, $heartbeat);
6666

67-
if (preg_match('/step = (\d+)/', $rrdInfo, $stepMatches) && $stepMatches[1] == $step) {
68-
preg_match_all('/minimal_heartbeat = (\d+)/', $rrdInfo, $heartbeatMatches);
69-
70-
$allOk = true;
71-
foreach ($heartbeatMatches[1] as $dsHeartbeat) {
72-
if ((int) $dsHeartbeat === (int) $heartbeat) {
73-
continue;
74-
}
75-
76-
$allOk = false;
77-
$this->line(__('commands.maintenance:rrd-step.mismatched_heartbeat', ['file' => $file, 'ds' => $dsHeartbeat, 'hb' => $heartbeat]));
78-
break;
79-
}
80-
81-
if ($allOk) {
82-
if ($this->getOutput()->isVerbose()) {
83-
$this->info(__('commands.maintenance:rrd-step.skipping', ['file' => $file, 'step' => $step]));
84-
}
85-
$skipped++;
86-
continue;
67+
if ($this->getOutput()->isVerbose()) {
68+
$this->info(__('commands.maintenance:rrd-step.skipping', ['file' => $file, 'step' => $step]));
8769
}
70+
$skipped++;
71+
continue;
72+
} catch (\RuntimeException $e) {
73+
$this->line($e->getMessage()); // inconsistent data found
74+
} catch (RrdException $e) {
75+
$this->error($e->getMessage());
76+
77+
return 1;
8878
}
8979

90-
$this->getOutput()->write(__('commands.maintenance:rrd-step.converting', ['file' => $file]) . ' ');
91-
92-
$commands = [
93-
"$rrdtool dump $daemon " . escapeshellarg($file) . ' > ' . escapeshellarg($random),
94-
"sed -i 's/<step>\\([0-9]*\\)/<step>$step/' " . escapeshellarg($random),
95-
"sed -i 's/<minimal_heartbeat>\\([0-9]*\\)/<minimal_heartbeat>$heartbeat/' " . escapeshellarg($random),
96-
"$rrdtool restore $daemon -f " . escapeshellarg($random) . ' ' . escapeshellarg($file),
97-
'rm -f ' . escapeshellarg($random),
98-
];
80+
try {
81+
$this->getOutput()->write(__('commands.maintenance:rrd-step.converting', ['file' => $file]) . ' ');
9982

100-
exec(implode(' && ', $commands), $output, $code);
83+
$xmlContent = $rrdProcess->run("dump $file");
84+
$modifiedXml = $this->modifyXml($xmlContent, $step, $heartbeat);
85+
$rrdProcess->run("restore -f - $file\n$modifiedXml\x1A");
10186

102-
if ($code === 0) {
10387
$this->info('[OK]');
10488
$converted++;
105-
} else {
106-
$this->error('[FAIL]');
89+
} catch (Exception $e) {
90+
$this->error('[FAIL]: ' . $e->getMessage());
10791
$failed++;
10892
}
10993
}
11094

111-
$this->line(__('commands.maintenance:rrd-step.summary', ['converted' => $converted, 'failed' => $failed, 'skipped' => $skipped]));
95+
$this->line(__('commands.maintenance:rrd-step.summary', [
96+
'converted' => $converted,
97+
'failed' => $failed,
98+
'skipped' => $skipped,
99+
]));
112100

113101
return $failed > 0 ? 1 : 0;
114102
}
103+
104+
/**
105+
* @throws RrdException
106+
*/
107+
private function checkRrdFile(RrdProcess $rrdProcess, string $file, int $step, int $heartbeat): void
108+
{
109+
$rrdInfo = $rrdProcess->run("info $file");
110+
111+
if (! preg_match('/step = (\d+)/', $rrdInfo, $stepMatches) || $stepMatches[1] != $step) {
112+
throw new \RuntimeException(__('commands.maintenance:rrd-step.mismatched_heartbeat', ['file' => $file, 'ds' => 'step', 'hb' => $step]));
113+
}
114+
115+
preg_match_all('/minimal_heartbeat = (\d+)/', $rrdInfo, $heartbeatMatches);
116+
117+
foreach ($heartbeatMatches[1] as $dsHeartbeat) {
118+
if ((int) $dsHeartbeat !== $heartbeat) {
119+
throw new \RuntimeException(__('commands.maintenance:rrd-step.mismatched_heartbeat', ['file' => $file, 'ds' => $dsHeartbeat, 'hb' => $heartbeat]));
120+
}
121+
}
122+
}
123+
124+
private function modifyXml(string $xmlContent, int $step, int $heartbeat): string
125+
{
126+
$xmlContent = (string) preg_replace('#<step>\d+</step>#', "<step>$step</step>", $xmlContent);
127+
$xmlContent = (string) preg_replace('#<minimal_heartbeat>\d+</minimal_heartbeat>#', "<minimal_heartbeat>$heartbeat</minimal_heartbeat>", $xmlContent);
128+
129+
return $xmlContent;
130+
}
115131
}

0 commit comments

Comments
 (0)