diff --git a/app/Console/Commands/Ping.php b/app/Console/Commands/Ping.php new file mode 100644 index 0000000000..b6edd00808 --- /dev/null +++ b/app/Console/Commands/Ping.php @@ -0,0 +1,46 @@ +alert("Do not use this command yet, use ./ping.php"); + exit(); + + PingCheck::dispatch(new PingCheck($this->argument('groups'))); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 6da222af72..3d0a5fc880 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -14,6 +14,7 @@ class Kernel extends ConsoleKernel */ protected $commands = [ Commands\Release::class, + Commands\Ping::class, ]; /** diff --git a/app/Jobs/PingCheck.php b/app/Jobs/PingCheck.php new file mode 100644 index 0000000000..616c1d3861 --- /dev/null +++ b/app/Jobs/PingCheck.php @@ -0,0 +1,305 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2018 Tony Murray + * @author Tony Murray + */ + +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('/^(?[^\s]+): (?:Name or service not known|Temporary failure in name resolution)/', $line, $errored)) { + $this->recordData([ + 'hostname' => $errored['hostname'], + 'status' => 'unreachable' + ]); + } + continue; + } + + if (preg_match( + '/^(?[^\s]+) is (?alive|unreachable)(?: \((?[\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])); + } + } +} diff --git a/app/Models/Device.php b/app/Models/Device.php index c67119a72b..170163d868 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -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'); diff --git a/app/Models/DevicePerf.php b/app/Models/DevicePerf.php new file mode 100644 index 0000000000..2a0e9e9147 --- /dev/null +++ b/app/Models/DevicePerf.php @@ -0,0 +1,65 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2018 Tony Murray + * @author Tony Murray + */ + +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'); + } +} diff --git a/composer.json b/composer.json index 975ddb41ea..39271a810e 100644 --- a/composer.json +++ b/composer.json @@ -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.*", diff --git a/composer.lock b/composer.lock index a05007d6e8..d0e08e1b55 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/daily.php b/daily.php index b96479371b..cc98f2b7a5 100644 --- a/daily.php +++ b/daily.php @@ -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); + } +} diff --git a/daily.sh b/daily.sh index 8e4729c87c..c99eb4c8e5 100755 --- a/daily.sh +++ b/daily.sh @@ -255,6 +255,7 @@ main () { # Cleanups local options=("refresh_alert_rules" "refresh_os_cache" + "recalculate_device_dependencies" "syslog" "eventlog" "authlog" diff --git a/doc/Extensions/Fast-Ping-Check.md b/doc/Extensions/Fast-Ping-Check.md new file mode 100644 index 0000000000..acb98ffa38 --- /dev/null +++ b/doc/Extensions/Fast-Ping-Check.md @@ -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. diff --git a/html/ajax_form.php b/html/ajax_form.php index 8efd22ed82..b1f2f04019 100644 --- a/html/ajax_form.php +++ b/html/ajax_form.php @@ -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); diff --git a/html/includes/forms/delete-host-dependency.inc.php b/html/includes/forms/delete-host-dependency.inc.php index f9938c4969..7a7defed71 100644 --- a/html/includes/forms/delete-host-dependency.inc.php +++ b/html/includes/forms/delete-host-dependency.inc.php @@ -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); diff --git a/html/includes/forms/save-host-dependency.inc.php b/html/includes/forms/save-host-dependency.inc.php index db8cc70abe..f041ef4d62 100644 --- a/html/includes/forms/save-host-dependency.inc.php +++ b/html/includes/forms/save-host-dependency.inc.php @@ -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); diff --git a/html/pages/device/edit/device.inc.php b/html/pages/device/edit/device.inc.php index 32527c3503..dc3f079ca6 100644 --- a/html/pages/device/edit/device.inc.php +++ b/html/pages/device/edit/device.inc.php @@ -1,5 +1,6 @@ $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']); diff --git a/includes/functions.php b/includes/functions.php index 78dd3500e2..91a8f2fbd8 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -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 diff --git a/misc/db_schema.yaml b/misc/db_schema.yaml index 78e907dcf2..ab5180a5ca 100644 --- a/misc/db_schema.yaml +++ b/misc/db_schema.yaml @@ -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 } diff --git a/mkdocs.yml b/mkdocs.yml index 09353f00bd..9af25539fa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/ping.php b/ping.php new file mode 100755 index 0000000000..b8a8baea4e --- /dev/null +++ b/ping.php @@ -0,0 +1,45 @@ +#!/usr/bin/env php + */ +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/\([0-9]*\)/$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"; diff --git a/sql-schema/257.sql b/sql-schema/257.sql new file mode 100644 index 0000000000..9d6014c385 --- /dev/null +++ b/sql-schema/257.sql @@ -0,0 +1 @@ +ALTER TABLE devices ADD max_depth int DEFAULT 0 NOT NULL;