Issues (2963)

app/Jobs/PingCheck.php (2 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 <https://www.gnu.org/licenses/>.
19
 *
20
 * @link       https://www.librenms.org
21
 *
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\Alert\AlertRules;
38
use LibreNMS\Config;
39
use LibreNMS\RRD\RrdDefinition;
40
use LibreNMS\Util\Debug;
41
use Log;
42
use Symfony\Component\Process\Process;
43
44
class PingCheck implements ShouldQueue
45
{
46
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
0 ignored issues
show
The trait Illuminate\Queue\SerializesModels requires some properties which are not provided by App\Jobs\PingCheck: $id, $relations, $class, $keyBy
Loading history...
47
48
    private $command;
49
    private $wait;
50
    private $rrd_tags;
51
52
    /** @var \Illuminate\Database\Eloquent\Collection List of devices keyed by hostname */
53
    private $devices;
54
    /** @var array List of device group ids to check */
55
    private $groups = [];
56
57
    // working data for loop
58
    /** @var Collection */
59
    private $tiered;
60
    /** @var Collection */
61
    private $current;
62
    private $current_tier;
63
    /** @var Collection */
64
    private $deferred;
65
66
    /**
67
     * Create a new job instance.
68
     *
69
     * @param  array  $groups  List of distributed poller groups to check
70
     */
71
    public function __construct($groups = [])
72
    {
73
        if (is_array($groups)) {
0 ignored issues
show
The condition is_array($groups) is always true.
Loading history...
74
            $this->groups = $groups;
75
        }
76
77
        // define rrd tags
78
        $rrd_step = Config::get('ping_rrd_step', Config::get('rrd.step', 300));
79
        $rrd_def = RrdDefinition::make()->addDataset('ping', 'GAUGE', 0, 65535, $rrd_step * 2);
80
        $this->rrd_tags = ['rrd_def' => $rrd_def, 'rrd_step' => $rrd_step];
81
82
        // set up fping process
83
        $timeout = Config::get('fping_options.timeout', 500); // must be smaller than period
84
        $retries = Config::get('fping_options.retries', 2);  // how many retries on failure
85
86
        $this->command = ['fping', '-f', '-', '-e', '-t', $timeout, '-r', $retries];
87
        $this->wait = Config::get('rrd.step', 300) * 2;
88
    }
89
90
    /**
91
     * Execute the job.
92
     *
93
     * @return void
94
     */
95
    public function handle()
96
    {
97
        $ping_start = microtime(true);
98
99
        $this->fetchDevices();
100
101
        $process = new Process($this->command, null, null, null, $this->wait);
102
103
        d_echo($process->getCommandLine() . PHP_EOL);
104
105
        // send hostnames to stdin to avoid overflowing cli length limits
106
        $ordered_device_list = $this->tiered->get(1, collect())->keys()// root nodes before standalone nodes
107
        ->merge($this->devices->keys())
108
            ->unique()
109
            ->implode(PHP_EOL);
110
111
        $process->setInput($ordered_device_list);
112
        $process->start(); // start as early as possible
113
114
        foreach ($process as $type => $line) {
115
            d_echo($line);
116
117
            if (Process::ERR === $type) {
118
                // Check for devices we couldn't resolve dns for
119
                if (preg_match('/^(?<hostname>[^\s]+): (?:Name or service not known|Temporary failure in name resolution)/', $line, $errored)) {
120
                    $this->recordData([
121
                        'hostname' => $errored['hostname'],
122
                        'status' => 'unreachable',
123
                    ]);
124
                }
125
                continue;
126
            }
127
128
            if (preg_match(
129
                '/^(?<hostname>[^\s]+) is (?<status>alive|unreachable)(?: \((?<rtt>[\d.]+) ms\))?/',
130
                $line,
131
                $captured
132
            )) {
133
                $this->recordData($captured);
134
135
                $this->processTier();
136
            }
137
        }
138
139
        // check for any left over devices
140
        if ($this->deferred->isNotEmpty()) {
141
            d_echo("Leftover devices, this shouldn't happen: " . $this->deferred->flatten(1)->implode('hostname', ', ') . PHP_EOL);
142
            d_echo('Devices left in tier: ' . collect($this->current)->implode('hostname', ', ') . PHP_EOL);
143
        }
144
145
        if (\App::runningInConsole()) {
146
            printf("Pinged %s devices in %.2fs\n", $this->devices->count(), microtime(true) - $ping_start);
147
        }
148
    }
149
150
    private function fetchDevices()
151
    {
152
        if (isset($this->devices)) {
153
            return $this->devices;
154
        }
155
156
        /** @var Builder $query */
157
        $query = Device::canPing()
158
            ->select(['devices.device_id', 'hostname', 'overwrite_ip', 'status', 'status_reason', 'last_ping', 'last_ping_timetaken', 'max_depth'])
159
            ->orderBy('max_depth');
160
161
        if ($this->groups) {
162
            $query->whereIn('poller_group', $this->groups);
163
        }
164
165
        $this->devices = $query->get()->keyBy(function ($device) {
166
            return Device::pollerTarget(json_decode(json_encode($device), true));
167
        });
168
169
        // working collections
170
        $this->tiered = $this->devices->groupBy('max_depth', true);
171
        $this->deferred = collect();
172
173
        // start with tier 1 (the root nodes, 0 is standalone)
174
        $this->current_tier = 1;
175
        $this->current = $this->tiered->get($this->current_tier, collect());
176
177
        if (Debug::isVerbose()) {
178
            $this->tiered->each(function (Collection $tier, $index) {
179
                echo "Tier $index (" . $tier->count() . '): ';
180
                echo $tier->implode('hostname', ', ');
181
                echo PHP_EOL;
182
            });
183
        }
184
185
        return $this->devices;
186
    }
187
188
    /**
189
     * Check if this tier is complete and move to the next tier
190
     * If we moved to the next tier, check if we can report any of our deferred results
191
     */
192
    private function processTier()
193
    {
194
        if ($this->current->isNotEmpty()) {
195
            return;
196
        }
197
198
        $this->current_tier++;  // next tier
199
200
        if (! $this->tiered->has($this->current_tier)) {
201
            // out of devices
202
            return;
203
        }
204
205
        if (Debug::isVerbose()) {
206
            echo "Out of devices at this tier, moving to tier $this->current_tier\n";
207
        }
208
209
        $this->current = $this->tiered->get($this->current_tier);
210
211
        // update and remove devices in the current tier
212
        foreach ($this->deferred->pull($this->current_tier, []) as $data) {
213
            $this->recordData($data);
214
        }
215
216
        // try to process the new tier in case we took care of all the devices
217
        $this->processTier();
218
    }
219
220
    /**
221
     * If the device is on the current tier, record the data and remove it
222
     * $data should have keys: hostname, status, and conditionally rtt
223
     *
224
     * @param  array  $data
225
     */
226
    private function recordData(array $data)
227
    {
228
        if (Debug::isVerbose()) {
229
            echo "Attempting to record data for {$data['hostname']}... ";
230
        }
231
232
        /** @var Device $device */
233
        $device = $this->devices->get($data['hostname']);
234
235
        // process the data if this is a standalone device or in the current tier
236
        if ($device->max_depth === 0 || $this->current->has($device->hostname)) {
237
            if (Debug::isVerbose()) {
238
                echo "Success\n";
239
            }
240
241
            // mark up only if snmp is not down too
242
            $device->status = ($data['status'] == 'alive' && $device->status_reason != 'snmp');
243
            $device->last_ping = Carbon::now();
244
            $device->last_ping_timetaken = isset($data['rtt']) ? $data['rtt'] : 0;
245
246
            if ($device->isDirty('status')) {
247
                // if changed, update reason
248
                $device->status_reason = $device->status ? '' : 'icmp';
249
                $type = $device->status ? 'up' : 'down';
250
                Log::event('Device status changed to ' . ucfirst($type) . ' from icmp check.', $device->device_id, $type);
251
            }
252
253
            $device->save(); // only saves if needed (which is every time because of last_ping)
254
255
            if (isset($type)) { // only run alert rules if status changed
256
                echo "Device $device->hostname changed status to $type, running alerts\n";
257
                $rules = new AlertRules;
258
                $rules->runRules($device->device_id);
259
            }
260
261
            // add data to rrd
262
            app('Datastore')->put($device->toArray(), 'ping-perf', $this->rrd_tags, ['ping' => $device->last_ping_timetaken]);
263
264
            // done with this device
265
            $this->complete($device->hostname);
266
            d_echo("Recorded data for $device->hostname (tier $device->max_depth)\n");
267
        } else {
268
            if (Debug::isVerbose()) {
269
                echo "Deferred\n";
270
            }
271
272
            $this->defer($data);
273
        }
274
    }
275
276
    /**
277
     * Done processing $hostname, remove it from our active data
278
     *
279
     * @param  string  $hostname
280
     */
281
    private function complete($hostname)
282
    {
283
        $this->current->offsetUnset($hostname);
284
        $this->deferred->each->offsetUnset($hostname);
285
    }
286
287
    /**
288
     * Defer this data processing until all parent devices are complete
289
     *
290
     *
291
     * @param  array  $data
292
     */
293
    private function defer(array $data)
294
    {
295
        $device = $this->devices->get($data['hostname']);
296
297
        if ($this->deferred->has($device->max_depth)) {
298
            // add this data to the proper tier, unless it already exists...
299
            $tier = $this->deferred->get($device->max_depth);
300
            if (! $tier->has($device->hostname)) {
301
                $tier->put($device->hostname, $data);
302
            }
303
        } else {
304
            // create a new tier containing this data
305
            $this->deferred->put($device->max_depth, collect([$device->hostname => $data]));
306
        }
307
    }
308
}
309