Allow ping checks to be ran separately from polling (#8821)

Allows ping checks at intervals not tied to the poller.  Pointless if you are not alerting on device status.
I updated the rrdstep.php script to treat ping-perf files separately and made it so it only converts if needed.

Docs here: https://docs.librenms.org/Extensions/Fast-Ping-Check/

DO NOT DELETE THIS TEXT

#### Please note

> Please read this information carefully. You can run `./scripts/pre-commit.php` to check your code before submitting.

- [x] Have you followed our [code guidelines?](http://docs.librenms.org/Developing/Code-Guidelines/)

#### Testers

If you would like to test this pull request then please run: `./scripts/github-apply <pr_id>`, i.e `./scripts/github-apply 5926`
This commit is contained in:
Tony Murray
2018-07-30 16:58:38 -05:00
committed by Neil Lathwood
parent 344dfb9797
commit 9bc0c542a5
20 changed files with 864 additions and 112 deletions

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands;
use App\Jobs\PingCheck;
use Illuminate\Console\Command;
class Ping extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ping {--d|debug} {groups?* | Optional List of distributed poller groups to poll}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check if devices are up or down via icmp';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->alert("Do not use this command yet, use ./ping.php");
exit();
PingCheck::dispatch(new PingCheck($this->argument('groups')));
}
}

View File

@@ -14,6 +14,7 @@ class Kernel extends ConsoleKernel
*/
protected $commands = [
Commands\Release::class,
Commands\Ping::class,
];
/**

305
app/Jobs/PingCheck.php Normal file
View File

@@ -0,0 +1,305 @@
<?php
/**
* PingCheck.php
*
* Device up/down icmp check job
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2018 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Jobs;
use App\Models\Device;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use LibreNMS\Config;
use LibreNMS\RRD\RrdDefinition;
use Symfony\Component\Process\Process;
class PingCheck implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $process;
private $rrd_tags;
/** @var \Illuminate\Database\Eloquent\Collection $devices List of devices keyed by hostname */
private $devices;
/** @var array $groups List of device group ids to check */
private $groups = [];
// working data for loop
/** @var Collection $tiered */
private $tiered;
/** @var Collection $current */
private $current;
private $current_tier;
/** @var Collection $deferred */
private $deferred;
/**
* Create a new job instance.
*
* @param array $groups List of distributed poller groups to check
*/
public function __construct($groups = [])
{
if (is_array($groups)) {
$this->groups = $groups;
}
// define rrd tags
$rrd_step = Config::get('ping_rrd_step', Config::get('rrd.step', 300));
$rrd_def = RrdDefinition::make()->addDataset('ping', 'GAUGE', 0, 65535, $rrd_step * 2);
$this->rrd_tags = ['rrd_def' => $rrd_def, 'rrd_step' => $rrd_step];
// set up fping process
$timeout = Config::get('fping_options.timeout', 500); // must be smaller than period
$retries = Config::get('fping_options.retries', 2); // how many retries on failure
$cmd = ['fping', '-f', '-', '-e', '-t', $timeout, '-r', $retries];
$wait = Config::get('rrd_step', 300) * 2;
$this->process = new Process($cmd, null, null, null, $wait);
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$ping_start = microtime(true);
$this->fetchDevices();
d_echo($this->process->getCommandLine() . PHP_EOL);
// send hostnames to stdin to avoid overflowing cli length limits
$ordered_device_list = $this->tiered->get(1, collect())->keys()// root nodes before standalone nodes
->merge($this->devices->keys())
->unique()
->implode(PHP_EOL);
$this->process->setInput($ordered_device_list);
$this->process->start(); // start as early as possible
foreach ($this->process as $type => $line) {
d_echo($line);
if (Process::ERR === $type) {
// Check for devices we couldn't resolve dns for
if (preg_match('/^(?<hostname>[^\s]+): (?:Name or service not known|Temporary failure in name resolution)/', $line, $errored)) {
$this->recordData([
'hostname' => $errored['hostname'],
'status' => 'unreachable'
]);
}
continue;
}
if (preg_match(
'/^(?<hostname>[^\s]+) is (?<status>alive|unreachable)(?: \((?<rtt>[\d.]+) ms\))?/',
$line,
$captured
)) {
$this->recordData($captured);
$this->processTier();
}
}
// check for any left over devices
if ($this->deferred->isNotEmpty()) {
d_echo("Leftover devices, this shouldn't happen: " . $this->deferred->flatten(1)->implode('hostname', ', ') . PHP_EOL);
d_echo("Devices left in tier: " . collect($this->current)->implode('hostname', ', ') . PHP_EOL);
}
if (\App::runningInConsole()) {
printf("Pinged %s devices in %.2fs\n", $this->devices->count(), microtime(true) - $ping_start);
}
}
private function fetchDevices()
{
if (isset($this->devices)) {
return $this->devices;
}
global $vdebug;
/** @var Builder $query */
$query = Device::canPing()
->select(['devices.device_id', 'hostname', 'status', 'status_reason', 'last_ping', 'last_ping_timetaken', 'max_depth'])
->orderBy('max_depth');
if ($this->groups) {
$query->whereIn('poller_group', $this->groups);
}
$this->devices = $query->get()->keyBy('hostname');
// working collections
$this->tiered = $this->devices->groupBy('max_depth', true);
$this->deferred = collect();
// start with tier 1 (the root nodes, 0 is standalone)
$this->current_tier = 1;
$this->current = $this->tiered->get($this->current_tier, collect());
if ($vdebug) {
$this->tiered->each(function (Collection $tier, $index) {
echo "Tier $index (" . $tier->count() . "): ";
echo $tier->implode('hostname', ', ');
echo PHP_EOL;
});
}
return $this->devices;
}
/**
* Check if this tier is complete and move to the next tier
* If we moved to the next tier, check if we can report any of our deferred results
*/
private function processTier()
{
global $vdebug;
if ($this->current->isNotEmpty()) {
return;
}
$this->current_tier++; // next tier
if (!$this->tiered->has($this->current_tier)) {
// out of devices
return;
}
if ($vdebug) {
echo "Out of devices at this tier, moving to tier $this->current_tier\n";
}
$this->current = $this->tiered->get($this->current_tier);
// update and remove devices in the current tier
foreach ($this->deferred->pull($this->current_tier, []) as $data) {
$this->recordData($data);
}
// try to process the new tier in case we took care of all the devices
$this->processTier();
}
/**
* If the device is on the current tier, record the data and remove it
* $data should have keys: hostname, status, and conditionally rtt
*
* @param $data
*/
private function recordData($data)
{
global $vdebug;
if ($vdebug) {
echo "Attempting to record data for {$data['hostname']}... ";
}
/** @var Device $device */
$device = $this->devices->get($data['hostname']);
// process the data if this is a standalone device or in the current tier
if ($device->max_depth === 0 || $this->current->has($device->hostname)) {
if ($vdebug) {
echo "Success\n";
}
// mark up only if snmp is not down too
$device->status = ($data['status'] == 'alive' && $device->status_reason != 'snmp');
$device->last_ping = Carbon::now();
$device->last_ping_timetaken = isset($data['rtt']) ? $data['rtt'] : 0;
if ($device->isDirty('status')) {
// if changed, update reason
$device->status_reason = $device->status ? '' : 'icmp';
$type = $device->status ? 'up' : 'down';
log_event('Device status changed to ' . ucfirst($type) . " from icmp check.", $device->toArray(), $type);
echo "Device $device->hostname changed status to $type, running alerts\n";
RunRules($device->device_id);
}
$device->save(); // only saves if needed (which is every time because of last_ping)
// add data to rrd
data_update($device->toArray(), 'ping-perf', $this->rrd_tags, ['ping' => $device->last_ping_timetaken]);
// done with this device
$this->complete($device->hostname);
d_echo("Recorded data for $device->hostname (tier $device->max_depth)\n");
} else {
if ($vdebug) {
echo "Deferred\n";
}
$this->defer($data);
}
}
/**
* Done processing $hostname, remove it from our active data
*
* @param $hostname
*/
private function complete($hostname)
{
$this->current->offsetUnset($hostname);
$this->deferred->each->offsetUnset($hostname);
}
/**
* Defer this data processing until all parent devices are complete
*
*
* @param $data
*/
private function defer($data)
{
$device = $this->devices->get($data['hostname']);
if ($this->deferred->has($device->max_depth)) {
// add this data to the proper tier, unless it already exists...
$tier = $this->deferred->get($device->max_depth);
if (!$tier->has($device->hostname)) {
$tier->put($device->hostname, $data);
}
} else {
// create a new tier containing this data
$this->deferred->put($device->max_depth, collect([$device->hostname => $data]));
}
}
}

View File

@@ -2,11 +2,18 @@
namespace App\Models;
use Fico7489\Laravel\Pivot\Traits\PivotEventTrait;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
class Device extends BaseModel
{
use PivotEventTrait;
public $timestamps = false;
protected $primaryKey = 'device_id';
protected $fillable = ['hostname', 'ip', 'status', 'status_reason'];
protected $casts = ['status' => 'boolean'];
/**
* Initialize this class
@@ -20,6 +27,56 @@ class Device extends BaseModel
$device->ports()->delete();
$device->syslogs()->delete();
$device->eventlogs()->delete();
// handle device dependency updates
$device->children->each->updateMaxDepth($device->device_id);
});
// handle device dependency updates
static::updated(function (Device $device) {
if ($device->isDirty('max_depth')) {
$device->children->each->updateMaxDepth();
}
});
static::pivotAttached(function (Device $device, $relationName, $pivotIds, $pivotIdsAttributes) {
if ($relationName == 'parents') {
// a parent attached to this device
// update the parent's max depth incase it used to be standalone
Device::whereIn('device_id', $pivotIds)->get()->each->validateStandalone();
// make sure this device's max depth is updated
$device->updateMaxDepth();
} elseif ($relationName == 'children') {
// a child device attached to this device
// if this device used to be standalone, we need to udpate max depth
$device->validateStandalone();
// make sure the child's max depth is updated
Device::whereIn('device_id', $pivotIds)->get()->each->updateMaxDepth();
}
});
static::pivotDetached(function (Device $device, $relationName, $pivotIds) {
if ($relationName == 'parents') {
// this device detached from a parent
// update this devices max depth
$device->updateMaxDepth();
// parent may now be standalone, update old parent
Device::whereIn('device_id', $pivotIds)->get()->each->validateStandalone();
} elseif ($relationName == 'children') {
// a child device detached from this device
// update the detached child's max_depth
Device::whereIn('device_id', $pivotIds)->get()->each->updateMaxDepth();
// this device may be standalone, update it
$device->validateStandalone();
}
});
}
@@ -47,6 +104,53 @@ class Device extends BaseModel
return asset('images/os/generic.svg');
}
/**
* Update the max_depth field based on parents
* Performs SQL query, so make sure all parents are saved first
*
* @param int $exclude exclude a device_id from being considered (used for deleting)
*/
public function updateMaxDepth($exclude = null)
{
// optimize for memory instead of time
$query = $this->parents()->getQuery();
if (!is_null($exclude)) {
$query->where('device_id', '!=', $exclude);
}
$count = $query->count();
if ($count === 0) {
if ($this->children()->count() === 0) {
$this->max_depth = 0; // no children or parents
} else {
$this->max_depth = 1; // has children
}
} else {
$parents_max_depth = $query->max('max_depth');
$this->max_depth = $parents_max_depth + 1;
}
$this->save();
}
/**
* Device dependency check to see if this node is standalone or not.
* Standalone is a special case where the device has no parents or children and is denoted by a max_depth of 0
*
* Only checks on root nodes (where max_depth is 1 or 0)
*
*/
public function validateStandalone()
{
if ($this->max_depth === 0 && $this->children()->count() > 0) {
$this->max_depth = 1; // has children
} elseif ($this->max_depth === 1 && $this->parents()->count() === 0) {
$this->max_depth = 0; // no children or parents
}
$this->save();
}
/**
* @return string
*/
@@ -70,9 +174,9 @@ class Device extends BaseModel
public function getIconAttribute($icon)
{
if (isset($icon)) {
return asset("images/os/$icon");
return "images/os/$icon";
}
return asset('images/os/generic.svg');
return 'images/os/generic.svg';
}
public function getIpAttribute($ip)
{
@@ -88,6 +192,11 @@ class Device extends BaseModel
$this->attributes['ip'] = inet_pton($ip);
}
public function setStatusAttribute($status)
{
$this->attributes['status'] = (int)$status;
}
// ---- Query scopes ----
public function scopeIsUp($query)
@@ -138,6 +247,19 @@ class Device extends BaseModel
]);
}
public function scopeCanPing(Builder $query)
{
return $query->where('disabled', 0)
->leftJoin('devices_attribs', function (JoinClause $query) {
$query->on('devices.device_id', 'devices_attribs.device_id')
->where('devices_attribs.attrib_type', 'override_icmp_disable');
})
->where(function (Builder $query) {
$query->whereNull('devices_attribs.attrib_value')
->orWhere('devices_attribs.attrib_value', '!=', 'true');
});
}
public function scopeHasAccess($query, User $user)
{
return $this->hasDeviceAccess($query, $user);
@@ -165,6 +287,11 @@ class Device extends BaseModel
return $this->hasMany('App\Models\CefSwitching', 'device_id');
}
public function children()
{
return $this->belongsToMany('App\Models\Device', 'device_relationships', 'parent_device_id', 'child_device_id');
}
public function components()
{
return $this->hasMany('App\Models\Component', 'device_id');
@@ -190,6 +317,16 @@ class Device extends BaseModel
return $this->hasMany('App\Models\Package', 'device_id', 'device_id');
}
public function parents()
{
return $this->belongsToMany('App\Models\Device', 'device_relationships', 'child_device_id', 'parent_device_id');
}
public function perf()
{
return $this->hasMany('App\Models\DevicePerf', 'device_id');
}
public function ports()
{
return $this->hasMany('App\Models\Port', 'device_id', 'device_id');

65
app/Models/DevicePerf.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
/**
* DevicePerf.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2018 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DevicePerf extends BaseModel
{
protected $table = 'device_perf';
protected $fillable = ['device_id', 'timestamp', 'xmt', 'rcv', 'loss', 'min', 'max', 'avg'];
protected $casts = [
'xmt' => 'integer',
'rcv' => 'integer',
'loss' => 'integer',
'min' => 'float',
'max' => 'float',
'avg' => 'float',
];
public $timestamps = false;
const CREATED_AT = 'timestamp';
protected $attributes = [
'min' => 0,
'max' => 0,
'avg' => 0,
];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->timestamp = $model->freshTimestamp();
});
}
// ---- Define Relationships ----
public function device()
{
return $this->belongsTo('App\Models\Device', 'device_id', 'device_id');
}
}

View File

@@ -45,6 +45,7 @@
"laravel/laravel": "5.4.*",
"oriceon/toastr-5-laravel": "dev-master",
"wpb/string-blade-compiler": "3.4.x-dev",
"fico7489/laravel-pivot": "*",
"vlucas/phpdotenv": "2.4.0",
"doctrine/inflector": "1.1.*",

144
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d962cf418393daf777aa51f999b40933",
"content-hash": "f57760426989ee971f25f10f73d7d661",
"packages": [
{
"name": "amenadiel/jpgraph",
@@ -419,6 +419,56 @@
],
"time": "2018-02-23T01:58:20+00:00"
},
{
"name": "fico7489/laravel-pivot",
"version": "2.0.6",
"source": {
"type": "git",
"url": "https://github.com/fico7489/laravel-pivot.git",
"reference": "f4197fb797b0c544e18ee47d8a9407b2ade0930c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fico7489/laravel-pivot/zipball/f4197fb797b0c544e18ee47d8a9407b2ade0930c",
"reference": "f4197fb797b0c544e18ee47d8a9407b2ade0930c",
"shasum": ""
},
"require": {
"illuminate/database": "5.4.*"
},
"require-dev": {
"orchestra/testbench": "3.4.*",
"phpunit/phpunit": "~5.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Fico7489\\Laravel\\Pivot\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Filip Horvat",
"email": "filip.horvat@am2studio.hr",
"homepage": "http://am2studio.hr",
"role": "Developer"
}
],
"description": "This package introduces new eloquent events for sync(), attach(), detach() or updateExistingPivot() methods on BelongsToMany relation.",
"homepage": "https://github.com/fico7489/laravel-pivot",
"keywords": [
"eloquent events",
"eloquent extra events",
"laravel BelongsToMany events",
"laravel pivot events",
"laravel sync events"
],
"time": "2018-03-08T16:05:59+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.3.3",
@@ -2080,21 +2130,22 @@
},
{
"name": "ramsey/uuid",
"version": "3.7.3",
"version": "3.8.0",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76"
"reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/44abcdad877d9a46685a3a4d221e3b2c4b87cb76",
"reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/d09ea80159c1929d75b3f9c60504d613aeb4a1e3",
"reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3",
"shasum": ""
},
"require": {
"paragonie/random_compat": "^1.0|^2.0",
"php": "^5.4 || ^7.0"
"paragonie/random_compat": "^1.0|^2.0|9.99.99",
"php": "^5.4 || ^7.0",
"symfony/polyfill-ctype": "^1.8"
},
"replace": {
"rhumsaa/uuid": "self.version"
@@ -2102,16 +2153,17 @@
"require-dev": {
"codeception/aspect-mock": "^1.0 | ~2.0.0",
"doctrine/annotations": "~1.2.0",
"goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1",
"goaop/framework": "1.0.0-alpha.2 | ^1.0 | ~2.1.0",
"ircmaxell/random-lib": "^1.1",
"jakub-onderka/php-parallel-lint": "^0.9.0",
"mockery/mockery": "^0.9.9",
"moontoast/math": "^1.1",
"php-mock/php-mock-phpunit": "^0.3|^1.1",
"phpunit/phpunit": "^4.7|^5.0",
"phpunit/phpunit": "^4.7|^5.0|^6.5",
"squizlabs/php_codesniffer": "^2.3"
},
"suggest": {
"ext-ctype": "Provides support for PHP Ctype functions",
"ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator",
"ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator",
"ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
@@ -2156,7 +2208,7 @@
"identifier",
"uuid"
],
"time": "2018-01-20T00:28:24+00:00"
"time": "2018-07-19T23:38:55+00:00"
},
{
"name": "rmccue/requests",
@@ -2310,16 +2362,16 @@
},
{
"name": "symfony/console",
"version": "v3.4.12",
"version": "v3.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "1b97071a26d028c9bd4588264e101e14f6e7cd00"
"reference": "e54f84c50e3b12972e7750edfc5ca84b2284c44e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/1b97071a26d028c9bd4588264e101e14f6e7cd00",
"reference": "1b97071a26d028c9bd4588264e101e14f6e7cd00",
"url": "https://api.github.com/repos/symfony/console/zipball/e54f84c50e3b12972e7750edfc5ca84b2284c44e",
"reference": "e54f84c50e3b12972e7750edfc5ca84b2284c44e",
"shasum": ""
},
"require": {
@@ -2375,11 +2427,11 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2018-05-23T05:02:55+00:00"
"time": "2018-07-10T14:02:11+00:00"
},
{
"name": "symfony/css-selector",
"version": "v3.4.12",
"version": "v3.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
@@ -2432,16 +2484,16 @@
},
{
"name": "symfony/debug",
"version": "v3.4.12",
"version": "v3.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "47e6788c5b151cf0cfdf3329116bf33800632d75"
"reference": "0e3ca9cbde90fffec8038f4d4e16fd4046bbd018"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/47e6788c5b151cf0cfdf3329116bf33800632d75",
"reference": "47e6788c5b151cf0cfdf3329116bf33800632d75",
"url": "https://api.github.com/repos/symfony/debug/zipball/0e3ca9cbde90fffec8038f4d4e16fd4046bbd018",
"reference": "0e3ca9cbde90fffec8038f4d4e16fd4046bbd018",
"shasum": ""
},
"require": {
@@ -2484,11 +2536,11 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2018-06-25T11:10:40+00:00"
"time": "2018-06-26T08:45:54+00:00"
},
{
"name": "symfony/dotenv",
"version": "v3.4.12",
"version": "v3.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/dotenv.git",
@@ -2545,7 +2597,7 @@
},
{
"name": "symfony/event-dispatcher",
"version": "v3.4.12",
"version": "v3.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
@@ -2608,7 +2660,7 @@
},
{
"name": "symfony/finder",
"version": "v3.4.12",
"version": "v3.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
@@ -2657,16 +2709,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v3.4.12",
"version": "v3.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "1c28679fcbb0d9b35e4fd49fbb74d2ca4ea17bce"
"reference": "2b8e08c085e2dc7449ee6d55a238be87d3727c96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/1c28679fcbb0d9b35e4fd49fbb74d2ca4ea17bce",
"reference": "1c28679fcbb0d9b35e4fd49fbb74d2ca4ea17bce",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/2b8e08c085e2dc7449ee6d55a238be87d3727c96",
"reference": "2b8e08c085e2dc7449ee6d55a238be87d3727c96",
"shasum": ""
},
"require": {
@@ -2707,20 +2759,20 @@
],
"description": "Symfony HttpFoundation Component",
"homepage": "https://symfony.com",
"time": "2018-06-21T11:10:19+00:00"
"time": "2018-07-19T07:08:28+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v3.4.12",
"version": "v3.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "cb7edcdc47cab3c61c891e6e55337f8dd470d820"
"reference": "22a1d000d45f09966a363223548a150aec759e61"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/cb7edcdc47cab3c61c891e6e55337f8dd470d820",
"reference": "cb7edcdc47cab3c61c891e6e55337f8dd470d820",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/22a1d000d45f09966a363223548a150aec759e61",
"reference": "22a1d000d45f09966a363223548a150aec759e61",
"shasum": ""
},
"require": {
@@ -2796,7 +2848,7 @@
],
"description": "Symfony HttpKernel Component",
"homepage": "https://symfony.com",
"time": "2018-06-25T12:29:19+00:00"
"time": "2018-07-23T16:37:31+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -2973,16 +3025,16 @@
},
{
"name": "symfony/process",
"version": "v3.4.12",
"version": "v3.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "acc5a37c706ace827962851b69705b24e71ca17c"
"reference": "f741672edfcfe3a2ea77569d419006f23281d909"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/acc5a37c706ace827962851b69705b24e71ca17c",
"reference": "acc5a37c706ace827962851b69705b24e71ca17c",
"url": "https://api.github.com/repos/symfony/process/zipball/f741672edfcfe3a2ea77569d419006f23281d909",
"reference": "f741672edfcfe3a2ea77569d419006f23281d909",
"shasum": ""
},
"require": {
@@ -3018,7 +3070,7 @@
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"time": "2018-05-30T04:24:30+00:00"
"time": "2018-07-09T09:01:07+00:00"
},
{
"name": "symfony/routing",
@@ -3161,16 +3213,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v3.4.12",
"version": "v3.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "e173954a28a44a32c690815fbe4d0f2eac43accb"
"reference": "c501f46bb1eaf4c8d65ba070ab65a1986da1cd7f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/e173954a28a44a32c690815fbe4d0f2eac43accb",
"reference": "e173954a28a44a32c690815fbe4d0f2eac43accb",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/c501f46bb1eaf4c8d65ba070ab65a1986da1cd7f",
"reference": "c501f46bb1eaf4c8d65ba070ab65a1986da1cd7f",
"shasum": ""
},
"require": {
@@ -3226,11 +3278,11 @@
"debug",
"dump"
],
"time": "2018-06-15T07:47:49+00:00"
"time": "2018-07-09T08:21:26+00:00"
},
{
"name": "symfony/yaml",
"version": "v2.8.42",
"version": "v2.8.43",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
@@ -4969,7 +5021,7 @@
},
{
"name": "symfony/class-loader",
"version": "v3.4.12",
"version": "v3.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/class-loader.git",

View File

@@ -6,6 +6,8 @@
* (c) 2013 LibreNMS Contributors
*/
use App\Models\Device;
use Illuminate\Database\Eloquent\Collection;
use LibreNMS\Config;
use LibreNMS\Exceptions\LockException;
use LibreNMS\Util\MemcacheLock;
@@ -294,3 +296,29 @@ if ($options['f'] === 'refresh_os_cache') {
echo 'Clearing OS cache' . PHP_EOL;
unlink(Config::get('install_dir') . '/cache/os_defs.cache');
}
if ($options['f'] === 'recalculate_device_dependencies') {
// fix broken dependency max_depth calculation in case things weren't done though eloquent
try {
if (Config::get('distributed_poller')) {
MemcacheLock::lock('recalculate_device_dependencies', 0, 86000);
}
\LibreNMS\DB\Eloquent::boot();
// update all root nodes and recurse, chunk so we don't blow up
Device::doesntHave('parents')->with('children')->chunk(100, function (Collection $devices) {
// anonymous recursive function
$recurse = function (Device $device) use (&$recurse) {
$device->updateMaxDepth();
$device->children->each($recurse);
};
$devices->each($recurse);
});
} catch (LockException $e) {
echo $e->getMessage() . PHP_EOL;
exit(-1);
}
}

View File

@@ -255,6 +255,7 @@ main () {
# Cleanups
local options=("refresh_alert_rules"
"refresh_os_cache"
"recalculate_device_dependencies"
"syslog"
"eventlog"
"authlog"

View File

@@ -0,0 +1,64 @@
source: Extensions/Fast-Ping-Check.md
## Fast up/down checking
Normally, LibreNMS sends an ICMP ping to the device before polling to check if it is up or down.
This check is tied to the poller frequency, which is normally 5 minutes. This means it may take up to 5 minutes
to find out if a device is down.
Some users may want to know if devices stop responding to ping more quickly than that. LibreNMS offers a ping.php script
to run ping checks as quickly as possible without increasing snmp load on your devices by switching to 1 minute polling.
> **WARNING**: If you do not have an alert rule that alerts on device status, enabling this will be a waste of resources.
> You can find one in the [Alert Rules Collection](../Alerting/Rules.md#Alert Rules Collection).
### Setting the ping check to 1 minute
1. Change the ping_rrd_step setting in config.php
```php
$config['ping_rrd_step'] = 60;
```
2. Update the rrd files to change the step (step is hardcoded at file creation in rrd files)
```bash
./scripts/rrdstep.php -h all
```
3. Add the following line to /etc/cron.d/librenms.nonroot.cron to allow 1 minute ping checks
```
* * * * * librenms /opt/librenms/ping.php >> /dev/null 2>&1
```
#### Sub minute ping check
Cron only has a resolution of one minute, so we have to use a trick to allow sub minute checks.
We add two entries, but add a delay before one.
>Alerts are only run every minute, so you will have to modify them as well. Remove the original alerts.php entry.
1. Set ping_rrd_step
```php
$config['ping_rrd_step'] = 30;
```
2. Update the rrd files
```bash
./scripts/rrdstep.php -h all
```
3. Update cron (removing any other ping.php or alert.php entries)
```
* * * * * librenms /opt/librenms/ping.php >> /dev/null 2>&1
* * * * * librenms sleep 30 && /opt/librenms/ping.php >> /dev/null 2>&1
* * * * * librenms sleep 15 && /opt/librenms/alerts.php >> /dev/null 2>&1
* * * * * librenms sleep 45 && /opt/librenms/alerts.php >> /dev/null 2>&1
```
### Device dependencies
The ping.php script respects device dependencies, but the main poller does not (for technical reasons).
However, using this script does not disable the icmp check in the poller and a child may be reported as
down before the parent.

View File

@@ -16,7 +16,7 @@
use LibreNMS\Authentication\Auth;
$init_modules = array('web', 'auth', 'alerts', 'alerts-cli');
$init_modules = array('web', 'auth', 'alerts', 'eloquent');
require realpath(__DIR__ . '/..') . '/includes/init.php';
set_debug(isset($_REQUEST['debug']) ? $_REQUEST['debug'] : false);

View File

@@ -15,39 +15,34 @@
use LibreNMS\Authentication\Auth;
if (!Auth::user()->hasGlobalAdmin()) {
$status = array('status' => 1, 'message' => 'You need to be admin');
$status = ['status' => 1, 'message' => 'You need to be admin'];
} else {
if ($_POST['device_id']) {
if (!is_numeric($_POST['device_id'])) {
$status = array('status' => 1, 'message' => 'Wrong device id!');
$status = ['status' => 1, 'message' => 'Wrong device id!'];
} else {
if (dbDelete('device_relationships', '`child_device_id` = ?', array($_POST['device_id']))) {
$status = array('status' => 0, 'message' => 'Device dependency has been deleted.');
$device = \App\Models\Device::find($_POST['device_id']);
if ($device->parents()->detach()) {
$status = ['status' => 0, 'message' => 'Device dependency has been deleted.'];
} else {
$status = array('status' => 1, 'message' => 'Device dependency cannot be deleted.');
$status = ['status' => 1, 'message' => 'Device dependency cannot be deleted.'];
}
}
} elseif ($_POST['parent_ids']) {
$error = false;
$status = ['status' => 0, 'message' => 'Device dependencies has been deleted'];
foreach ($_POST['parent_ids'] as $parent) {
if (is_numeric($parent) && $parent != 0) {
if (!dbDelete('device_relationships', ' `parent_device_id` = ?', array($parent))) {
$error = true;
$status = array('status' => 1, 'message' => 'Device dependency cannot be deleted.');
$device = \App\Models\Device::find($_POST['device_id']);
if (!$device->children()->detach()) {
$status = ['status' => 1, 'message' => 'Device dependency cannot be deleted.'];
}
} elseif ($parent == 0) {
$status = array('status' => 1, 'message' => 'No dependency to delete.');
$error = true;
$status = ['status' => 1, 'message' => 'No dependency to delete.'];
break;
}
}
if (!$error) {
$status = array('status' => 0, 'message' => 'Device dependencies has been deleted');
} else {
}
}
}
header('Content-Type: application/json');
echo _json_encode($status);
echo json_encode($status);

View File

@@ -15,46 +15,35 @@
use LibreNMS\Authentication\Auth;
if (!Auth::user()->hasGlobalAdmin()) {
$status = array('status' => 1, 'message' => 'You need to be admin');
$status = ['status' => 1, 'message' => 'You need to be admin'];
} else {
foreach ($_POST['parent_ids'] as $parent) {
$parent_ids = (array)$_POST['parent_ids'];
$device_ids = (array)$_POST['device_ids'];
foreach ($parent_ids as $parent) {
if (!is_numeric($parent)) {
$status = array('status' => 1, 'message' => 'Parent ID must be an integer!');
$status = ['status' => 1, 'message' => 'Parent ID must be an integer!'];
break;
}
}
if (count($_POST['parent_ids']) > 1 && in_array('0', $_POST['parent_ids'])) {
$status = array('status' => 1, 'message' => 'Multiple parents cannot contain None-Parent!');
if (count($parent_ids) > 1 && in_array('0', $parent_ids)) {
$status = ['status' => 1, 'message' => 'Multiple parents cannot contain None-Parent!'];
}
// A bit of an effort to reuse this code with dependency editing and the dependency wizard (editing multiple hosts at the same time)
$device_arr = array();
foreach ($_POST['device_ids'] as $dev) {
if (!is_numeric($dev)) {
$status = array('status' => 1, 'message' => 'Device ID must be an integer!');
foreach ($device_ids as $device_id) {
if (!is_numeric($device_id)) {
$status = ['status' => 1, 'message' => 'Device ID must be an integer!'];
break;
} elseif (in_array($dev, $_POST['parent_ids'])) {
$status = array('status' => 1, 'message' => 'A device cannot depend itself');
} elseif (in_array($device_id, $parent_ids)) {
$status = ['status' => 1, 'message' => 'A device cannot depend itself'];
break;
}
$insert = array();
foreach ($_POST['parent_ids'] as $parent) {
if (is_numeric($parent) && $parent != 0) {
$insert[] = array('parent_device_id' => $parent, 'child_device_id' => $dev);
} elseif ($parent == 0) {
// In case we receive a mixed array with $parent = 0 (which shouldn't happen)
// Empty the insert array so we remove any previous dependency so 'None' takes precedence
$insert = array();
break;
}
}
dbDelete('device_relationships', '`child_device_id` = ?', array($dev));
if (!empty($insert)) {
dbBulkInsert($insert, 'device_relationships');
}
\App\Models\Device::find($device_id)->parents()->sync($parent_ids);
$status = array('status' => 0, 'message' => 'Device dependencies have been saved');
}
}
header('Content-Type: application/json');
echo _json_encode($status);
echo json_encode($status);

View File

@@ -1,5 +1,6 @@
<?php
use App\Models\Device;
use LibreNMS\Authentication\Auth;
if ($_POST['editing']) {
@@ -7,13 +8,9 @@ if ($_POST['editing']) {
$updated = 0;
if (isset($_POST['parent_id'])) {
$parent_id = array_diff((array)$_POST['parent_id'], ['0']);
$res = dbDelete('device_relationships', '`child_device_id` = ?', array($device['device_id']));
if (!in_array('0', $pr)) {
foreach ($parent_id as $pr) {
dbInsert(array('parent_device_id' => $pr, 'child_device_id' => $device['device_id']), 'device_relationships');
}
}
$parents = array_diff((array)$_POST['parent_id'], ['0']);
// TODO avoid loops!
Device::find($device['device_id'])->parents()->sync($parents);
}
$override_sysLocation_bool = mres($_POST['override_sysLocation']);

View File

@@ -24,6 +24,7 @@ use LibreNMS\Exceptions\LockException;
use LibreNMS\Exceptions\SnmpVersionUnsupportedException;
use LibreNMS\Util\IP;
use LibreNMS\Util\MemcacheLock;
use Monolog\Logger;
/**
* Set debugging output

View File

@@ -472,6 +472,7 @@ devices:
- { Field: override_sysLocation, Type: tinyint(1), 'Null': true, Extra: '', Default: '0' }
- { Field: notes, Type: text, 'Null': true, Extra: '' }
- { Field: port_association_mode, Type: int(11), 'Null': false, Extra: '', Default: '1' }
- { Field: max_depth, Type: int(11), 'Null': false, Extra: '', Default: '0' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [device_id], Unique: true, Type: BTREE }
status: { Name: status, Columns: [status], Unique: false, Type: BTREE }

View File

@@ -53,6 +53,7 @@ pages:
- Extensions/Dashboards.md
- 5. Advanced Setup:
- Support/1-Minute-Polling.md
- Fast Ping Checking: Extensions/Fast-Ping-Check.md
- Configuration docs: Support/Configuration.md
- Authentication Options: Extensions/Authentication.md
- Two-Factor Auth: Extensions/Two-Factor-Auth.md

45
ping.php Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env php
<?php
use App\Jobs\PingCheck;
$init_modules = ['alerts', 'laravel', 'nodb'];
require __DIR__ . '/includes/init.php';
$options = getopt('hdvg:');
if (isset($options['h'])) {
echo <<<'END'
ping.php: Usage ping.php [-d] [-v] [-g group(s)]
-d enable debug output
-v enable verbose debug output
-g only ping devices for this poller group, may be comma separated list
END;
exit;
}
set_debug(isset($options['d']));
if (isset($options['v'])) {
global $vdebug;
$vdebug = true;
}
if (isset($options['g'])) {
$groups = explode(',', $options['g']);
} else {
$groups = [];
}
if ($config['noinfluxdb'] !== true && $config['influxdb']['enable'] === true) {
$influxdb = influxdb_connect();
} else {
$influxdb = false;
}
rrdtool_initialize();
PingCheck::dispatch(new PingCheck($groups));
rrdtool_close();

View File

@@ -24,6 +24,8 @@
* @author Neil Lathwood <neil@lathwood.co.uk>
*/
use LibreNMS\Config;
$init_modules = array();
require realpath(__DIR__ . '/..') . '/includes/init.php';
@@ -49,26 +51,42 @@ if (empty($hostname)) {
exit;
}
$step = $config['rrd']['step'];
$heartbeat = $config['rrd']['heartbeat'];
$rrdtool = $config['rrdtool'];
$tmp_path = $config['temp_dir'];
$system_step = Config::get('rrd.step', 300);
$icmp_step = Config::get('ping_rrd_step', $step);
$system_heartbeat = Config::get('rrd.heartbeat', $step * 2);
$rrdtool = Config::get('rrdtool', 'rrdtool');
$tmp_path = Config::get('temp_dir', '/tmp');
if ($hostname === 'all') {
$hostname = '*';
}
$files = glob(get_rrd_dir($hostname) . '/*.rrd');
$run = readline("Are you sure you want to run this command [N/y]: ");
if (!($run == 'y' || $run == 'Y')) {
echo "Exiting....." . PHP_EOL;
exit;
}
$converted = 0;
$skipped = 0;
$failed = 0;
foreach ($files as $file) {
$random = $tmp_path.'/'.mt_rand() . '.xml';
$tmp = explode('/', $file);
$rrd_file = array_pop($tmp);
$rrd_file = basename($file, '.rrd');
if ($rrd_file == 'ping-perf') {
$step = $icmp_step;
$heartbeat = $icmp_step * 2;
} else {
$step = $system_step;
$heartbeat = $system_heartbeat;
}
$rrd_info = shell_exec("$rrdtool info $file");
preg_match('/step = (\d+)/', $rrd_info, $matches);
if ($matches[1] == $step) {
d_echo("Skipping $file, step is already $step.\n");
$skipped++;
continue;
}
echo "Converting $file: ";
$command = "$rrdtool dump $file > $random &&
sed -i 's/<step>\([0-9]*\)/<step>$step/' $random &&
@@ -78,7 +96,11 @@ foreach ($files as $file) {
exec($command, $output, $code);
if ($code === 0) {
echo "[OK]\n";
$converted++;
} else {
echo "\033[FAIL]\n";
$failed++;
}
}
echo "Converted: $converted Failed: $failed Skipped: $skipped\n";

1
sql-schema/257.sql Normal file
View File

@@ -0,0 +1 @@
ALTER TABLE devices ADD max_depth int DEFAULT 0 NOT NULL;