Skip to content

Commit 7f6daac

Browse files
authored
Update maintenance:rrd-step to work with rrdcached (librenms#18623)
* 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) * Revert changes to Rrd datastore and use local code. getRrdFiles is just too much of a mess to use. If someone wants to fix it, they can do so in a separate PR. * Style fixes
1 parent c6ab22d commit 7f6daac

File tree

2 files changed

+100
-56
lines changed

2 files changed

+100
-56
lines changed

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: 93 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,38 @@
55
use App\Console\LnmsCommand;
66
use App\Facades\DeviceCache;
77
use App\Facades\LibrenmsConfig;
8-
use App\Facades\Rrd;
9-
use Illuminate\Support\Str;
8+
use Exception;
9+
use LibreNMS\Exceptions\RrdException;
10+
use LibreNMS\RRD\RrdProcess;
1011
use Symfony\Component\Console\Input\InputArgument;
1112
use Symfony\Component\Console\Input\InputOption;
1213

1314
class MaintenanceRrdStep extends LnmsCommand
1415
{
1516
protected $name = 'maintenance:rrd-step';
1617

18+
private int $systemStep;
19+
private int $icmpStep;
20+
private int $systemHeartbeat;
21+
1722
public function __construct()
1823
{
1924
parent::__construct();
2025
$this->addArgument('device', InputArgument::REQUIRED);
2126
$this->addOption('confirm', null, InputOption::VALUE_NONE);
2227
}
2328

24-
public function handle(): int
29+
public function handle(RrdProcess $rrdProcess): int
2530
{
2631
$this->configureOutputOptions();
2732

33+
$this->systemStep = (int) LibrenmsConfig::get('rrd.step', 300);
34+
$this->icmpStep = (int) LibrenmsConfig::get('ping_rrd_step', $this->systemStep);
35+
$this->systemHeartbeat = (int) LibrenmsConfig::get('rrd.heartbeat', $this->systemStep * 2);
36+
2837
$hostname = (string) $this->argument('device');
2938
if ($hostname !== 'all') {
30-
$hostname = DeviceCache::get($hostname)->hostname;
39+
$hostname = DeviceCache::get($hostname)->hostname; // hostname may be numeric
3140
}
3241

3342
if (empty($hostname)) {
@@ -40,76 +49,105 @@ public function handle(): int
4049
return 0;
4150
}
4251

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') ?: [];
54-
5552
[$converted, $skipped, $failed] = [0, 0, 0];
5653

57-
foreach ($files as $file) {
58-
$random = rtrim($tmpPath, '/') . '/' . mt_rand() . '.xml';
54+
foreach ($this->listFiles($hostname, $rrdProcess) as $file) {
5955
$rrdFile = basename($file, '.rrd');
6056

6157
[$step, $heartbeat] = $rrdFile === 'icmp-perf'
62-
? [$icmpStep, $icmpStep * 2]
63-
: [$systemStep, $systemHeartbeat];
58+
? [$this->icmpStep, $this->icmpStep * 2]
59+
: [$this->systemStep, $this->systemHeartbeat];
6460

65-
$rrdInfo = shell_exec("$rrdtool info $daemon " . escapeshellarg($file)) ?? '';
66-
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-
}
61+
try {
62+
$this->checkRrdFile($rrdProcess, $file, $step, $heartbeat);
8063

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;
64+
if ($this->getOutput()->isVerbose()) {
65+
$this->info(__('commands.maintenance:rrd-step.skipping', ['file' => $file, 'step' => $step]));
8766
}
67+
$skipped++;
68+
continue;
69+
} catch (\RuntimeException $e) {
70+
$this->line($e->getMessage()); // inconsistent data found
71+
} catch (RrdException $e) {
72+
$this->error($e->getMessage());
73+
74+
return 1;
8875
}
8976

90-
$this->getOutput()->write(__('commands.maintenance:rrd-step.converting', ['file' => $file]) . ' ');
77+
try {
78+
$this->getOutput()->write(__('commands.maintenance:rrd-step.converting', ['file' => $file]) . ' ');
9179

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+
$xmlContent = $rrdProcess->run("dump $file");
81+
$modifiedXml = $this->modifyXml($xmlContent, $step, $heartbeat);
82+
$rrdProcess->run("restore -f - $file\n$modifiedXml\x1A");
9983

100-
exec(implode(' && ', $commands), $output, $code);
101-
102-
if ($code === 0) {
10384
$this->info('[OK]');
10485
$converted++;
105-
} else {
106-
$this->error('[FAIL]');
86+
} catch (Exception $e) {
87+
$this->error('[FAIL]: ' . $e->getMessage());
10788
$failed++;
10889
}
10990
}
11091

111-
$this->line(__('commands.maintenance:rrd-step.summary', ['converted' => $converted, 'failed' => $failed, 'skipped' => $skipped]));
92+
$this->line(__('commands.maintenance:rrd-step.summary', [
93+
'converted' => $converted,
94+
'failed' => $failed,
95+
'skipped' => $skipped,
96+
]));
11297

11398
return $failed > 0 ? 1 : 0;
11499
}
100+
101+
/**
102+
* @throws RrdException
103+
*/
104+
private function checkRrdFile(RrdProcess $rrdProcess, string $file, int $step, int $heartbeat): void
105+
{
106+
$rrdInfo = $rrdProcess->run("info $file");
107+
108+
if (! preg_match('/step = (\d+)/', $rrdInfo, $stepMatches) || $stepMatches[1] != $step) {
109+
throw new \RuntimeException(__('commands.maintenance:rrd-step.mismatched_heartbeat', ['file' => $file, 'ds' => 'step', 'hb' => $step]));
110+
}
111+
112+
preg_match_all('/minimal_heartbeat = (\d+)/', $rrdInfo, $heartbeatMatches);
113+
114+
foreach ($heartbeatMatches[1] as $dsHeartbeat) {
115+
if ((int) $dsHeartbeat !== $heartbeat) {
116+
throw new \RuntimeException(__('commands.maintenance:rrd-step.mismatched_heartbeat', ['file' => $file, 'ds' => $dsHeartbeat, 'hb' => $heartbeat]));
117+
}
118+
}
119+
}
120+
121+
private function modifyXml(string $xmlContent, int $step, int $heartbeat): string
122+
{
123+
$xmlContent = (string) preg_replace('#<step>\d+</step>#', "<step>$step</step>", $xmlContent);
124+
$xmlContent = (string) preg_replace('#<minimal_heartbeat>\d+</minimal_heartbeat>#', "<minimal_heartbeat>$heartbeat</minimal_heartbeat>", $xmlContent);
125+
126+
return $xmlContent;
127+
}
128+
129+
/**
130+
* @param string $hostname
131+
* @param RrdProcess $rrdProcess
132+
* @return string[]
133+
*
134+
* @throws RrdException
135+
*/
136+
private function listFiles(string $hostname, RrdProcess $rrdProcess): array
137+
{
138+
$command = $hostname === 'all' ? ['list -r .', ''] : "list ./$hostname";
139+
$output = rtrim($rrdProcess->run($command));
140+
141+
if (empty($output)) {
142+
return [];
143+
}
144+
145+
$files = explode("\n", $output);
146+
147+
if ($hostname === 'all') {
148+
return $files;
149+
}
150+
151+
return array_map(fn ($file) => "$hostname/$file", $files);
152+
}
115153
}

0 commit comments

Comments
 (0)