Passed
Push — master ( 0baeb0...d4017c )
by Tony
19:18 queued 08:55
created

app/Jobs/PingCheck.php (3 issues)

1
<?php
2
/**
3
 * PingCheck.php
4
 *
5
 * Device up/down icmp check job
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 <http://www.gnu.org/licenses/>.
19
 *
20
 * @package    LibreNMS
21
 * @link       http://librenms.org
22
 * @copyright  2018 Tony Murray
23
 * @author     Tony Murray <[email protected]>
24
 */
25
26
namespace App\Jobs;
27
28
use App\Models\Device;
29
use Carbon\Carbon;
30
use Illuminate\Bus\Queueable;
31
use Illuminate\Contracts\Queue\ShouldQueue;
32
use Illuminate\Database\Eloquent\Builder;
33
use Illuminate\Foundation\Bus\Dispatchable;
34
use Illuminate\Queue\InteractsWithQueue;
35
use Illuminate\Queue\SerializesModels;
36
use Illuminate\Support\Collection;
37
use LibreNMS\Config;
38
use LibreNMS\RRD\RrdDefinition;
39
use Symfony\Component\Process\Process;
40
41
class PingCheck implements ShouldQueue
42
{
43
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
44
45
    private $process;
46
    private $rrd_tags;
47
48
    /** @var \Illuminate\Database\Eloquent\Collection $devices List of devices keyed by hostname */
49
    private $devices;
50
    /** @var array $groups List of device group ids to check */
51
    private $groups = [];
52
53
    // working data for loop
54
    /** @var Collection $tiered */
55
    private $tiered;
56
    /** @var Collection $current */
57
    private $current;
58
    private $current_tier;
59
    /** @var Collection $deferred */
60
    private $deferred;
61
62
    /**
63
     * Create a new job instance.
64
     *
65
     * @param array $groups List of distributed poller groups to check
66
     */
67
    public function __construct($groups = [])
68
    {
69
        if (is_array($groups)) {
70
            $this->groups = $groups;
71
        }
72
73
        // define rrd tags
74
        $rrd_step = Config::get('ping_rrd_step', Config::get('rrd.step', 300));
75
        $rrd_def = RrdDefinition::make()->addDataset('ping', 'GAUGE', 0, 65535, $rrd_step * 2);
76
        $this->rrd_tags = ['rrd_def' => $rrd_def, 'rrd_step' => $rrd_step];
77
78
        // set up fping process
79
        $timeout = Config::get('fping_options.timeout', 500); // must be smaller than period
80
        $retries = Config::get('fping_options.retries', 2);  // how many retries on failure
81
82
        $cmd = ['fping', '-f', '-', '-e', '-t', $timeout, '-r', $retries];
83
84
        $wait = Config::get('rrd.step', 300) * 2;
85
86
        $this->process = new Process($cmd, null, null, null, $wait);
87
    }
88
89
    /**
90
     * Execute the job.
91
     *
92
     * @return void
93
     */
94
    public function handle()
95
    {
96
        $ping_start = microtime(true);
97
98
        $this->fetchDevices();
99
100
        d_echo($this->process->getCommandLine() . PHP_EOL);
101
102
        // send hostnames to stdin to avoid overflowing cli length limits
103
        $ordered_device_list = $this->tiered->get(1, collect())->keys()// root nodes before standalone nodes
104
        ->merge($this->devices->keys())
105
            ->unique()
106
            ->implode(PHP_EOL);
107
108
        $this->process->setInput($ordered_device_list);
109
        $this->process->start(); // start as early as possible
110
111
        foreach ($this->process as $type => $line) {
112
            d_echo($line);
113
114
            if (Process::ERR === $type) {
115
                // Check for devices we couldn't resolve dns for
116
                if (preg_match('/^(?<hostname>[^\s]+): (?:Name or service not known|Temporary failure in name resolution)/', $line, $errored)) {
117
                    $this->recordData([
118
                        'hostname' => $errored['hostname'],
119
                        'status' => 'unreachable'
120
                    ]);
121
                }
122
                continue;
123
            }
124
125
            if (preg_match(
126
                '/^(?<hostname>[^\s]+) is (?<status>alive|unreachable)(?: \((?<rtt>[\d.]+) ms\))?/',
127
                $line,
128
                $captured
129
            )) {
130
                $this->recordData($captured);
131
132
                $this->processTier();
133
            }
134
        }
135
136
        // check for any left over devices
137
        if ($this->deferred->isNotEmpty()) {
138
            d_echo("Leftover devices, this shouldn't happen: " . $this->deferred->flatten(1)->implode('hostname', ', ') . PHP_EOL);
139
            d_echo("Devices left in tier: " . collect($this->current)->implode('hostname', ', ') . PHP_EOL);
140
        }
141
142
        if (\App::runningInConsole()) {
143
            printf("Pinged %s devices in %.2fs\n", $this->devices->count(), microtime(true) - $ping_start);
144
        }
145
    }
146
147
    private function fetchDevices()
148
    {
149
        if (isset($this->devices)) {
150
            return $this->devices;
151
        }
152
153
        global $vdebug;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
154
155
        /** @var Builder $query */
156
        $query = Device::canPing()
157
            ->select(['devices.device_id', 'hostname', 'status', 'status_reason', 'last_ping', 'last_ping_timetaken', 'max_depth'])
158
            ->orderBy('max_depth');
159
160
        if ($this->groups) {
161
            $query->whereIn('poller_group', $this->groups);
162
        }
163
164
        $this->devices = $query->get()->keyBy('hostname');
165
166
        // working collections
167
        $this->tiered = $this->devices->groupBy('max_depth', true);
168
        $this->deferred = collect();
169
170
        // start with tier 1 (the root nodes, 0 is standalone)
171
        $this->current_tier = 1;
172
        $this->current = $this->tiered->get($this->current_tier, collect());
173
174
        if ($vdebug) {
175
            $this->tiered->each(function (Collection $tier, $index) {
176
                echo "Tier $index (" . $tier->count() . "): ";
177
                echo $tier->implode('hostname', ', ');
178
                echo PHP_EOL;
179
            });
180
        }
181
182
        return $this->devices;
183
    }
184
185
    /**
186
     * Check if this tier is complete and move to the next tier
187
     * If we moved to the next tier, check if we can report any of our deferred results
188
     */
189
    private function processTier()
190
    {
191
        global $vdebug;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
192
193
        if ($this->current->isNotEmpty()) {
194
            return;
195
        }
196
197
        $this->current_tier++;  // next tier
198
199
        if (!$this->tiered->has($this->current_tier)) {
200
            // out of devices
201
            return;
202
        }
203
204
        if ($vdebug) {
205
            echo "Out of devices at this tier, moving to tier $this->current_tier\n";
206
        }
207
208
        $this->current = $this->tiered->get($this->current_tier);
209
210
        // update and remove devices in the current tier
211
        foreach ($this->deferred->pull($this->current_tier, []) as $data) {
212
            $this->recordData($data);
213
        }
214
215
        // try to process the new tier in case we took care of all the devices
216
        $this->processTier();
217
    }
218
219
    /**
220
     * If the device is on the current tier, record the data and remove it
221
     * $data should have keys: hostname, status, and conditionally rtt
222
     *
223
     * @param $data
224
     */
225
    private function recordData($data)
226
    {
227
        global $vdebug;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
228
229
        if ($vdebug) {
230
            echo "Attempting to record data for {$data['hostname']}... ";
231
        }
232
233
        /** @var Device $device */
234
        $device = $this->devices->get($data['hostname']);
235
236
        // process the data if this is a standalone device or in the current tier
237
        if ($device->max_depth === 0 || $this->current->has($device->hostname)) {
238
            if ($vdebug) {
239
                echo "Success\n";
240
            }
241
242
            // mark up only if snmp is not down too
243
            $device->status = ($data['status'] == 'alive' && $device->status_reason != 'snmp');
244
            $device->last_ping = Carbon::now();
245
            $device->last_ping_timetaken = isset($data['rtt']) ? $data['rtt'] : 0;
246
247
            if ($device->isDirty('status')) {
248
                // if changed, update reason
249
                $device->status_reason = $device->status ? '' : 'icmp';
250
                $type = $device->status ? 'up' : 'down';
251
                log_event('Device status changed to ' . ucfirst($type) . " from icmp check.", $device->toArray(), $type);
252
253
                echo "Device $device->hostname changed status to $type, running alerts\n";
254
                RunRules($device->device_id);
255
            }
256
            $device->save(); // only saves if needed (which is every time because of last_ping)
257
258
            // add data to rrd
259
            data_update($device->toArray(), 'ping-perf', $this->rrd_tags, ['ping' => $device->last_ping_timetaken]);
260
261
            // done with this device
262
            $this->complete($device->hostname);
263
            d_echo("Recorded data for $device->hostname (tier $device->max_depth)\n");
264
        } else {
265
            if ($vdebug) {
266
                echo "Deferred\n";
267
            }
268
269
            $this->defer($data);
270
        }
271
    }
272
273
    /**
274
     * Done processing $hostname, remove it from our active data
275
     *
276
     * @param $hostname
277
     */
278
    private function complete($hostname)
279
    {
280
        $this->current->offsetUnset($hostname);
281
        $this->deferred->each->offsetUnset($hostname);
282
    }
283
284
    /**
285
     * Defer this data processing until all parent devices are complete
286
     *
287
     *
288
     * @param $data
289
     */
290
    private function defer($data)
291
    {
292
        $device = $this->devices->get($data['hostname']);
293
294
        if ($this->deferred->has($device->max_depth)) {
295
            // add this data to the proper tier, unless it already exists...
296
            $tier = $this->deferred->get($device->max_depth);
297
            if (!$tier->has($device->hostname)) {
298
                $tier->put($device->hostname, $data);
299
            }
300
        } else {
301
            // create a new tier containing this data
302
            $this->deferred->put($device->max_depth, collect([$device->hostname => $data]));
303
        }
304
    }
305
}
306