55use App \Console \LnmsCommand ;
66use App \Facades \DeviceCache ;
77use 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 ;
1011use Symfony \Component \Console \Input \InputArgument ;
1112use Symfony \Component \Console \Input \InputOption ;
1213
1314class 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