1 | <?php |
||||
2 | /** |
||||
3 | * SmokepingGenerateCommand.php |
||||
4 | * |
||||
5 | * CLI command to generate a smokeping configuration. |
||||
6 | * |
||||
7 | * This program is free software: you can redistribute it and/or modify |
||||
8 | * it under the terms of the GNU General Public License as published by |
||||
9 | * the Free Software Foundation, either version 3 of the License, or |
||||
10 | * (at your option) any later version. |
||||
11 | * |
||||
12 | * This program is distributed in the hope that it will be useful, |
||||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the |
||||
15 | * GNU General Public License for more details. |
||||
16 | * |
||||
17 | * You should have received a copy of the GNU General Public License |
||||
18 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
||||
19 | * |
||||
20 | * @link https://www.librenms.org |
||||
21 | * |
||||
22 | * @copyright 2020 Adam Bishop |
||||
23 | * @author Adam Bishop <[email protected]> |
||||
24 | */ |
||||
25 | |||||
26 | namespace App\Console\Commands; |
||||
27 | |||||
28 | use App\Console\LnmsCommand; |
||||
29 | use App\Models\Device; |
||||
30 | use LibreNMS\Config; |
||||
31 | use Symfony\Component\Console\Input\InputOption; |
||||
32 | |||||
33 | class SmokepingGenerateCommand extends LnmsCommand |
||||
34 | { |
||||
35 | protected $name = 'smokeping:generate'; |
||||
36 | protected $dnsLookup = true; |
||||
37 | |||||
38 | private $ip4count = 0; |
||||
39 | private $ip6count = 0; |
||||
40 | private $warnings = []; |
||||
41 | |||||
42 | const IP4PROBE = 'lnmsFPing-'; |
||||
43 | const IP6PROBE = 'lnmsFPing6-'; |
||||
44 | |||||
45 | // These entries are solely used to appease the smokeping config parser and serve no function |
||||
46 | const DEFAULTIP4PROBE = 'FPing'; |
||||
47 | const DEFAULTIP6PROBE = 'FPing6'; |
||||
48 | const DEFAULTPROBE = self::DEFAULTIP4PROBE; |
||||
49 | |||||
50 | /** |
||||
51 | * Create a new command instance. |
||||
52 | * |
||||
53 | * @return void |
||||
54 | */ |
||||
55 | public function __construct() |
||||
56 | { |
||||
57 | parent::__construct(); |
||||
58 | |||||
59 | $this->setDescription(__('commands.smokeping:generate.description')); |
||||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
60 | |||||
61 | $this->addOption('probes', null, InputOption::VALUE_NONE); |
||||
62 | $this->addOption('targets', null, InputOption::VALUE_NONE); |
||||
63 | $this->addOption('no-header', null, InputOption::VALUE_NONE); |
||||
64 | $this->addOption('single-process', null, InputOption::VALUE_NONE); |
||||
65 | $this->addOption('no-dns', null, InputOption::VALUE_NONE); |
||||
66 | $this->addOption('compat', null, InputOption::VALUE_NONE); |
||||
67 | } |
||||
68 | |||||
69 | /** |
||||
70 | * Execute the console command. |
||||
71 | * |
||||
72 | * @return int |
||||
73 | */ |
||||
74 | public function handle() |
||||
75 | { |
||||
76 | if (! $this->validateOptions()) { |
||||
77 | return 1; |
||||
78 | } |
||||
79 | |||||
80 | $devices = Device::isNotDisabled()->orderBy('type')->orderBy('hostname')->get(); |
||||
81 | |||||
82 | if (sizeof($devices) < 1) { |
||||
83 | $this->error(__('commands.smokeping:generate.no-devices')); |
||||
0 ignored issues
–
show
It seems like
__('commands.smokeping:generate.no-devices') can also be of type array and array ; however, parameter $string of Illuminate\Console\Command::error() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
84 | |||||
85 | return 3; |
||||
86 | } |
||||
87 | |||||
88 | if ($this->option('probes')) { |
||||
89 | return $this->buildProbesConfiguration(); |
||||
90 | } elseif ($this->option('targets')) { |
||||
91 | return $this->buildTargetsConfiguration($devices); |
||||
92 | } |
||||
93 | |||||
94 | return 2; |
||||
95 | } |
||||
96 | |||||
97 | /** |
||||
98 | * Disable DNS lookups by the configuration builder |
||||
99 | * |
||||
100 | * @return bool |
||||
101 | */ |
||||
102 | public function disableDNSLookup() |
||||
103 | { |
||||
104 | return $this->dnsLookup = false; |
||||
105 | } |
||||
106 | |||||
107 | /** |
||||
108 | * Build and output the probe configuration |
||||
109 | * |
||||
110 | * @return int |
||||
111 | */ |
||||
112 | public function buildProbesConfiguration() |
||||
113 | { |
||||
114 | $probes = $this->assembleProbes(Config::get('smokeping.probes')); |
||||
115 | $header = $this->buildHeader($this->option('no-header'), $this->option('compat')); |
||||
116 | |||||
117 | return $this->render($header, $probes); |
||||
118 | } |
||||
119 | |||||
120 | /** |
||||
121 | * Build and output the target configuration |
||||
122 | * |
||||
123 | * @return int |
||||
124 | */ |
||||
125 | public function buildTargetsConfiguration($devices) |
||||
126 | { |
||||
127 | // Take the devices array and build it into a hierarchical list |
||||
128 | $smokelist = []; |
||||
129 | foreach ($devices as $device) { |
||||
130 | $smokelist[$device->type][$device->hostname] = ['transport' => $device->transport]; |
||||
131 | } |
||||
132 | |||||
133 | $targets = $this->buildTargets($smokelist, Config::get('smokeping.probes'), $this->option('single-process')); |
||||
134 | $header = $this->buildHeader($this->option('no-header'), $this->option('compat')); |
||||
135 | |||||
136 | return $this->render($header, $targets); |
||||
137 | } |
||||
138 | |||||
139 | /** |
||||
140 | * Set a warning to be emitted |
||||
141 | * |
||||
142 | * @return void |
||||
143 | */ |
||||
144 | public function setWarning($warning) |
||||
145 | { |
||||
146 | $this->warnings[] = sprintf('# %s', $warning); |
||||
147 | } |
||||
148 | |||||
149 | /** |
||||
150 | * Bring together the probe lists |
||||
151 | * |
||||
152 | * @param int $probeCount Number of processes to create |
||||
153 | * @return array |
||||
154 | */ |
||||
155 | public function assembleProbes($probeCount) |
||||
156 | { |
||||
157 | if ($probeCount < 1) { |
||||
158 | return []; |
||||
159 | } |
||||
160 | |||||
161 | return array_merge( |
||||
162 | $this->buildProbes('FPing', self::DEFAULTIP4PROBE, self::IP4PROBE, Config::get('fping'), $probeCount), |
||||
163 | $this->buildProbes('FPing6', self::DEFAULTIP6PROBE, self::IP6PROBE, Config::get('fping6'), $probeCount) |
||||
164 | ); |
||||
165 | } |
||||
166 | |||||
167 | /** |
||||
168 | * Determine if a list of probes is needed, and write one if so |
||||
169 | * |
||||
170 | * @param string $module The smokeping module to use for this probe (FPing or FPing6, typically) |
||||
171 | * @param string $defaultProbe A default probe, needed by the smokeping configuration parser |
||||
172 | * @param string $probe The first part of the probe name, e.g. 'lnmsFPing' or 'lnmsFPing6' |
||||
173 | * @param string $binary Path to the relevant probe binary (i.e. the output of `which fping` or `which fping6`) |
||||
174 | * @param int $probeCount Number of processes to create |
||||
175 | * @return array |
||||
176 | */ |
||||
177 | public function buildProbes($module, $defaultProbe, $probe, $binary, $probeCount) |
||||
178 | { |
||||
179 | $lines = []; |
||||
180 | |||||
181 | $lines[] = sprintf('+ %s', $module); |
||||
182 | $lines[] = sprintf(' binary = %s', $binary); |
||||
183 | $lines[] = ' blazemode = true'; |
||||
184 | $lines[] = sprintf('++ %s', $defaultProbe); |
||||
185 | |||||
186 | for ($i = 0; $i < $probeCount; $i++) { |
||||
187 | $lines[] = sprintf('++ %s%s', $probe, $i); |
||||
188 | } |
||||
189 | |||||
190 | $lines[] = ''; |
||||
191 | |||||
192 | return $lines; |
||||
193 | } |
||||
194 | |||||
195 | /** |
||||
196 | * Generate a header to append to the smokeping configuration file |
||||
197 | * |
||||
198 | * @return array |
||||
199 | */ |
||||
200 | public function buildHeader($noHeader, $compat) |
||||
201 | { |
||||
202 | $lines = []; |
||||
203 | |||||
204 | if ($compat) { |
||||
205 | $lines[] = ''; |
||||
206 | $lines[] = 'menu = Top'; |
||||
207 | $lines[] = 'title = Network Latency Grapher'; |
||||
208 | $lines[] = ''; |
||||
209 | } |
||||
210 | |||||
211 | if (! $noHeader) { |
||||
212 | $lines[] = sprintf('# %s', __('commands.smokeping:generate.header-first')); |
||||
213 | $lines[] = sprintf('# %s', __('commands.smokeping:generate.header-second')); |
||||
214 | $lines[] = sprintf('# %s', __('commands.smokeping:generate.header-third')); |
||||
215 | |||||
216 | return array_merge($lines, $this->warnings, ['']); |
||||
217 | } |
||||
218 | |||||
219 | return $lines; |
||||
220 | } |
||||
221 | |||||
222 | /** |
||||
223 | * Determine if a list of targets is needed, and write one if so |
||||
224 | * |
||||
225 | * @param array $smokelist A list of devices to create a a config block for |
||||
226 | * @return array |
||||
227 | */ |
||||
228 | public function buildTargets($smokelist, $probeCount, $singleProcess) |
||||
229 | { |
||||
230 | $lines = []; |
||||
231 | |||||
232 | foreach ($smokelist as $type => $devices) { |
||||
233 | if (empty($type)) { |
||||
234 | $type = 'Ungrouped'; |
||||
235 | } |
||||
236 | |||||
237 | $lines[] = sprintf('+ %s', $this->buildMenuEntry($type)); |
||||
238 | $lines[] = sprintf(' menu = %s', $type); |
||||
239 | $lines[] = sprintf(' title = %s', $type); |
||||
240 | |||||
241 | $lines[] = ''; |
||||
242 | |||||
243 | $lines = array_merge($lines, $this->buildDevices($devices, $probeCount, $singleProcess)); |
||||
244 | } |
||||
245 | |||||
246 | return $lines; |
||||
247 | } |
||||
248 | |||||
249 | /** |
||||
250 | * Check arguments passed are sensible |
||||
251 | * |
||||
252 | * @return bool |
||||
253 | */ |
||||
254 | private function validateOptions() |
||||
255 | { |
||||
256 | if (! Config::has('smokeping.probes') || |
||||
257 | ! Config::has('fping') || |
||||
258 | ! Config::has('fping6') |
||||
259 | ) { |
||||
260 | $this->error(__('commands.smokeping:generate.config-insufficient')); |
||||
0 ignored issues
–
show
It seems like
__('commands.smokeping:g...e.config-insufficient') can also be of type array and array ; however, parameter $string of Illuminate\Console\Command::error() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
261 | |||||
262 | return false; |
||||
263 | } |
||||
264 | |||||
265 | if (! ($this->option('probes') xor $this->option('targets'))) { |
||||
266 | $this->error(__('commands.smokeping:generate.args-nonsense')); |
||||
267 | |||||
268 | return false; |
||||
269 | } |
||||
270 | |||||
271 | if (Config::get('smokeping.probes') < 1) { |
||||
272 | $this->error(__('commands.smokeping:generate.no-probes')); |
||||
273 | |||||
274 | return false; |
||||
275 | } |
||||
276 | |||||
277 | if ($this->option('compat') && ! $this->option('targets')) { |
||||
278 | $this->error(__('commands.smokeping:generate.args-nonsense')); |
||||
279 | |||||
280 | return false; |
||||
281 | } |
||||
282 | |||||
283 | if ($this->option('no-dns')) { |
||||
284 | $this->disableDNSLookup(); |
||||
285 | } |
||||
286 | |||||
287 | return true; |
||||
288 | } |
||||
289 | |||||
290 | /** |
||||
291 | * Take config lines and output them to stdout |
||||
292 | * |
||||
293 | * @param array ...$blocks Blocks of smokeping configuration arranged in arrays of strings |
||||
294 | * @return int |
||||
295 | */ |
||||
296 | private function render(...$blocks) |
||||
297 | { |
||||
298 | foreach (array_merge(...$blocks) as $line) { |
||||
299 | $this->line($line); |
||||
300 | } |
||||
301 | |||||
302 | return 0; |
||||
303 | } |
||||
304 | |||||
305 | /** |
||||
306 | * Build the configuration for a set of devices inside a type block |
||||
307 | * |
||||
308 | * @param array $devices A list of devices to create a a config block for |
||||
309 | * @return array |
||||
310 | */ |
||||
311 | private function buildDevices($devices, $probeCount, $singleProcess) |
||||
312 | { |
||||
313 | $lines = []; |
||||
314 | |||||
315 | foreach ($devices as $hostname => $config) { |
||||
316 | if (! $this->dnsLookup || $this->deviceIsResolvable($hostname)) { |
||||
317 | $lines[] = sprintf('++ %s', $this->buildMenuEntry($hostname)); |
||||
318 | $lines[] = sprintf(' menu = %s', $hostname); |
||||
319 | $lines[] = sprintf(' title = %s', $hostname); |
||||
320 | |||||
321 | if (! $singleProcess) { |
||||
322 | $lines[] = sprintf(' probe = %s', $this->balanceProbes($config['transport'], $probeCount)); |
||||
323 | } |
||||
324 | |||||
325 | $lines[] = sprintf(' host = %s', $hostname); |
||||
326 | $lines[] = ''; |
||||
327 | } |
||||
328 | } |
||||
329 | |||||
330 | return $lines; |
||||
331 | } |
||||
332 | |||||
333 | /** |
||||
334 | * Smokeping refuses to load if it has an unresolvable host, so check for this |
||||
335 | * |
||||
336 | * @param string $hostname Hostname to be checked |
||||
337 | * @return bool |
||||
338 | */ |
||||
339 | private function deviceIsResolvable($hostname) |
||||
340 | { |
||||
341 | // First we check for IP literals, then for a dns entry, finally for a hosts entry due to a PHP/libc limitation |
||||
342 | // We look for the hosts entry last (and separately) as this only works for v4 - v6 host entries won't be found |
||||
343 | if (filter_var($hostname, FILTER_VALIDATE_IP) || checkdnsrr($hostname, 'ANY') || is_array(gethostbynamel($hostname))) { |
||||
0 ignored issues
–
show
|
|||||
344 | return true; |
||||
345 | } |
||||
346 | |||||
347 | $this->setWarning(sprintf('"%s" %s', $hostname, __('commands.smokeping:generate.dns-fail'))); |
||||
348 | |||||
349 | return false; |
||||
350 | } |
||||
351 | |||||
352 | /** |
||||
353 | * Rewrite menu entries to a format that smokeping finds acceptable |
||||
354 | * |
||||
355 | * @param string $entry The LibreNMS device hostname to rewrite |
||||
356 | * @return string |
||||
357 | */ |
||||
358 | private function buildMenuEntry($entry) |
||||
359 | { |
||||
360 | return str_replace(['.', ' '], '_', $entry); |
||||
361 | } |
||||
362 | |||||
363 | /** |
||||
364 | * Select a probe to use deterministically. |
||||
365 | * |
||||
366 | * @param string $transport The transport (udp or udp6) as per the device database entry |
||||
367 | * @return string |
||||
368 | */ |
||||
369 | private function balanceProbes($transport, $probeCount) |
||||
370 | { |
||||
371 | if ($transport === 'udp') { |
||||
372 | if ($probeCount === $this->ip4count) { |
||||
373 | $this->ip4count = 0; |
||||
374 | } |
||||
375 | |||||
376 | return sprintf('%s%s', self::IP4PROBE, $this->ip4count++); |
||||
377 | } |
||||
378 | |||||
379 | if ($probeCount === $this->ip6count) { |
||||
380 | $this->ip6count = 0; |
||||
381 | } |
||||
382 | |||||
383 | return sprintf('%s%s', self::IP6PROBE, $this->ip6count++); |
||||
384 | } |
||||
385 | } |
||||
386 |