Issues (2963)

app/Console/Commands/SmokepingGenerateCommand.php (4 issues)

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
It seems like __('commands.smokeping:generate.description') can also be of type array and array; however, parameter $description of Symfony\Component\Consol...mmand::setDescription() 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 ignore-type  annotation

59
        $this->setDescription(/** @scrutinizer ignore-type */ __('commands.smokeping:generate.description'));
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 ignore-type  annotation

83
            $this->error(/** @scrutinizer ignore-type */ __('commands.smokeping:generate.no-devices'));
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 ignore-type  annotation

260
            $this->error(/** @scrutinizer ignore-type */ __('commands.smokeping:generate.config-insufficient'));
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
The condition is_array(gethostbynamel($hostname)) is always true.
Loading history...
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