Updates to snmptrap handling (#9010)

* Updates to snmptrap handling
fix a bug in findDeviceByIP.  Add more tests for that.
Move handle outside of the Trap class, it doesn't fit.
Add developer docs.

* fix tests copy paste issue.

* Fix findByIp when port may not exist.

* Logging: Output context (and extra) if they exist

* Generic trap event logging and new config setting.
This commit is contained in:
Tony Murray
2018-08-14 01:56:16 -05:00
committed by Neil Lathwood
parent 5e8a144c2d
commit 4c6f917d9e
20 changed files with 279 additions and 49 deletions

View File

@@ -0,0 +1,59 @@
<?php
/**
* Dispatcher.php
*
* Creates the correct handler for the trap and then sends it the trap.
*
* 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 LibreNMS\Snmptrap;
use LibreNMS\Config;
use LibreNMS\Snmptrap\Handlers\Fallback;
use Log;
class Dispatcher
{
/**
* Instantiate the correct handler for this trap and call it's handle method
*
*/
public static function handle(Trap $trap)
{
if (empty($trap->getDevice())) {
Log::warning("Could not find device for trap", ['trap_text' => $trap->getRaw()]);
return false;
}
// note, this doesn't clear the resolved SnpmtrapHandler so only one per run
/** @var \LibreNMS\Interfaces\SnmptrapHandler $handler */
$handler = app(\LibreNMS\Interfaces\SnmptrapHandler::class, [$trap->getTrapOid()]);
$handler->handle($trap->getDevice(), $trap);
// log an event if appropriate
$fallback = $handler instanceof Fallback;
$logging = Config::get('snmptraps.eventlog', 'unhandled');
if ($logging == 'all' || ($fallback && $logging == 'unhandled')) {
log_event("SNMP trap received: " . $trap->getTrapOid(), $trap->getDevice()->toArray(), 'trap');
}
return !$fallback;
}
}

View File

@@ -23,7 +23,7 @@
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Snmptrap\Handler;
namespace LibreNMS\Snmptrap\Handlers;
use App\Models\Device;
use LibreNMS\Interfaces\SnmptrapHandler;

View File

@@ -23,7 +23,7 @@
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Snmptrap\Handler;
namespace LibreNMS\Snmptrap\Handlers;
use App\Models\Device;
use LibreNMS\Interfaces\SnmptrapHandler;

View File

@@ -23,7 +23,7 @@
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Snmptrap\Handler;
namespace LibreNMS\Snmptrap\Handlers;
use App\Models\Device;
use LibreNMS\Interfaces\SnmptrapHandler;

View File

@@ -23,7 +23,7 @@
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Snmptrap\Handler;
namespace LibreNMS\Snmptrap\Handlers;
use App\Models\Device;
use LibreNMS\Interfaces\SnmptrapHandler;

View File

@@ -23,7 +23,7 @@
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Snmptrap\Handler;
namespace LibreNMS\Snmptrap\Handlers;
use App\Models\Device;
use LibreNMS\Interfaces\SnmptrapHandler;

View File

@@ -23,7 +23,7 @@
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Snmptrap\Handler;
namespace LibreNMS\Snmptrap\Handlers;
use App\Models\Device;
use LibreNMS\Interfaces\SnmptrapHandler;

View File

@@ -27,7 +27,7 @@ namespace LibreNMS\Snmptrap;
use App\Models\Device;
use Illuminate\Support\Collection;
use LibreNMS\Snmptrap\Handler\Fallback;
use LibreNMS\Snmptrap\Handlers\Fallback;
use LibreNMS\Util\IP;
use Log;
@@ -70,27 +70,6 @@ class Trap
});
}
/**
* Instantiate the correct handler for this trap and call it's handle method
*
*/
public function handle()
{
$this->getDevice();
if (empty($this->device)) {
Log::warning("Could not find device for trap", ['trap_text' => $this->raw]);
return false;
}
// note, this doesn't clear the resolved SnpmtrapHandler so only one per run
/** @var \LibreNMS\Interfaces\SnmptrapHandler $handler */
$handler = app(\LibreNMS\Interfaces\SnmptrapHandler::class, [$this->getTrapOid()]);
$handler->handle($this->getDevice(), $this);
return !($handler instanceof Fallback);
}
/**
* Find the first in this trap by substring
*

View File

@@ -36,8 +36,9 @@ class CliColorFormatter extends \Monolog\Formatter\LineFormatter
$this->console = \App::runningInConsole();
parent::__construct(
"%message%\n",
"%message% %context% %extra%\n",
null,
true,
true
);
}
@@ -51,6 +52,7 @@ class CliColorFormatter extends \Monolog\Formatter\LineFormatter
} else {
$record['message'] = $this->console_color->strip($record['message']);
}
unset($record['context']['color']);
}
return parent::format($record);

View File

@@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use LibreNMS\Exceptions\InvalidIpException;
use LibreNMS\Util\IP;
use LibreNMS\Util\IPv4;
use LibreNMS\Util\IPv6;
@@ -93,6 +94,10 @@ class Device extends BaseModel
public static function findByIp($ip)
{
if (!IP::isValid($ip)) {
return null;
}
$device = static::where('hostname', $ip)->orWhere('ip', inet_pton($ip))->first();
if ($device) {
@@ -101,9 +106,12 @@ class Device extends BaseModel
try {
$ipv4 = new IPv4($ip);
return Ipv4Address::where('ipv4_address', $ipv4)
$port = Ipv4Address::where('ipv4_address', (string) $ipv4)
->with('port', 'port.device')
->firstOrFail()->port->device;
->firstOrFail()->port;
if ($port) {
return $port->device;
}
} catch (InvalidIpException $e) {
//
} catch (ModelNotFoundException $e) {
@@ -112,9 +120,12 @@ class Device extends BaseModel
try {
$ipv6 = new IPv6($ip);
return Ipv6Address::where('ipv6_address', $ipv6->uncompressed())
$port = Ipv6Address::where('ipv6_address', $ipv6->uncompressed())
->with(['port', 'port.device'])
->firstOrFail()->port->device;
->firstOrFail()->port;
if ($port) {
return $port->device;
}
} catch (InvalidIpException $e) {
//
} catch (ModelNotFoundException $e) {

View File

@@ -4,7 +4,7 @@ namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use LibreNMS\Interfaces\SnmptrapHandler;
use LibreNMS\Snmptrap\Handler\Fallback;
use LibreNMS\Snmptrap\Handlers\Fallback;
class SnmptrapProvider extends ServiceProvider
{

View File

@@ -2,10 +2,10 @@
return [
'trap_handlers' => [
'SNMPv2-MIB::authenticationFailure' => \LibreNMS\Snmptrap\Handler\AuthenticationFailure::class,
'BGP4-MIB::bgpEstablished' => \LibreNMS\Snmptrap\Handler\BgpEstablished::class,
'BGP4-MIB::bgpBackwardTransition' => \LibreNMS\Snmptrap\Handler\BgpBackwardTransition::class,
'IF-MIB::linkUp' => \LibreNMS\Snmptrap\Handler\LinkUp::class,
'IF-MIB::linkDown' => \LibreNMS\Snmptrap\Handler\LinkDown::class,
'SNMPv2-MIB::authenticationFailure' => \LibreNMS\Snmptrap\Handlers\AuthenticationFailure::class,
'BGP4-MIB::bgpEstablished' => \LibreNMS\Snmptrap\Handlers\BgpEstablished::class,
'BGP4-MIB::bgpBackwardTransition' => \LibreNMS\Snmptrap\Handlers\BgpBackwardTransition::class,
'IF-MIB::linkUp' => \LibreNMS\Snmptrap\Handlers\LinkUp::class,
'IF-MIB::linkDown' => \LibreNMS\Snmptrap\Handlers\LinkDown::class,
]
];

View File

@@ -68,9 +68,12 @@ $factory->define(\App\Models\Ipv4Address::class, function (Faker\Generator $fake
return [
'ipv4_address' => $ip->uncompressed(),
'ipv4_prefixlen' => $prefix,
'port_id' => function () {
return factory(\App\Models\Port::class)->create()->port_id;
},
'ipv4_network_id' => function () use ($ip) {
return factory(\App\Models\Ipv4Network::class)->create(['ipv4_network' => $ip->getNetworkAddress() . '/' . $ip->cidr])->ipv4_network_id;
}
},
];
});

View File

@@ -0,0 +1,50 @@
source: Developing/SNMP-Traps.md
# Creating snmp trap handlers
Create a new class in LibreNMS\Snmptrap\Handlers that implements the
LibreNMS\Interfaces\SnmptrapHandler interface.
Register the mapping in the config/snmptraps.php file. Make sure to use the full trap oid.
```php
'IF-MIB::linkUp' => \LibreNMS\Snmptrap\Handlers\LinkUp::class
```
The handle function inside your new class will receive a LibreNMS/Snmptrap/Trap
object containing the parsed trap. It is common to update the database and create
event log entries within the handle function.
### Getting information from the Trap
#### Source information
```php
$trap->getDevice(); // gets Device model for the device associated with this trap
$trap->getHostname(); // gets hostname sent with the trap
$trap->getIp(); // gets source IP of this trap
$trap->getTrapOid(); // returns the string you registered your class with
```
#### Retrieving data from the Trap
```php
$trap->getOidData('IF-MIB::ifDescr.114');
```
getOidData() requires the full name including any additional index.
You can use these functions to search the oid keys.
```php
$trap->findOid('ifDescr'); // returns the first oid key that contains the string
$trap->findOids('ifDescr'); // returns all oid keys containing the string
```
#### Advanced
If the above isn't adequate, you can get the entire trap text
```php
$trap->getRaw();
```

View File

@@ -2,6 +2,7 @@ source: Extensions/SNMP-Trap-Handler.md
# SNMP trap handling
Currently, librenms only supports linkUp/linkDown (port up/down), bgpEstablished/bgpBackwardTransition (BGP Sessions Up/Down) and authenticationFailure SNMP traps.
To add more see [Adding new SNMP Trap handlers](../Developing/SNMP-Traps.md)
Traps are handled via snmptrapd.
@@ -18,3 +19,18 @@ traphandle default /opt/librenms/snmptrap.php
```
Along with any necessary configuration to receive the traps from your devices (community, etc.)
### Event logging
You can configure generic event logging for snmp traps. This will log an event of the type trap for received traps.
These events can be utilized for alerting.
In config.php
```php
$config['snmptraps']['eventlog'] = 'unhandled';
```
Valid options are:
- `unhandled` only unhandled traps will be logged
- `all` log all traps
- `none` no traps will create a generic event log (handled traps may still log events)

View File

@@ -976,3 +976,6 @@ $config['api']['cors']['allowheaders'] = array('Origin', 'X-Requested-With', 'Co
// Disk
$config['bad_disk_regexp'] = [];
// Snmptrap logging: none, unhandled, all
$config['snmptraps']['eventlog'] = 'unhandled';

View File

@@ -145,6 +145,7 @@ pages:
- Developing/os/Custom-Graphs.md
- Developing/os/Settings.md
- Developing/Sensor-State-Support.md
- Developing/SNMP-Traps.md
- Developing/Dynamic-Config.md
- Developing/Merging-Pull-Requests.md
- Developing/Creating-Release.md

View File

@@ -23,6 +23,6 @@ if (set_debug(isset($options['d']))) {
}
$text = stream_get_contents(STDIN);
$trap = new \LibreNMS\Snmptrap\Trap($text);
$trap->handle(); // create handle and send it this trap
// create handle and send it this trap
\LibreNMS\Snmptrap\Dispatcher::handle(new \LibreNMS\Snmptrap\Trap($text));

View File

@@ -30,6 +30,7 @@ use App\Models\Device;
use App\Models\Ipv4Address;
use App\Models\Port;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use LibreNMS\Snmptrap\Dispatcher;
use LibreNMS\Snmptrap\Trap;
class SnmpTrapTest extends LaravelTestCase
@@ -41,7 +42,7 @@ class SnmpTrapTest extends LaravelTestCase
$trapText = "Garbage\n";
$trap = new Trap($trapText);
$this->assertFalse($trap->handle(), 'Found handler for trap with no snmpTrapOID');
$this->assertFalse(Dispatcher::handle($trap), 'Found handler for trap with no snmpTrapOID');
}
public function testFindByIp()
@@ -57,7 +58,7 @@ UDP: [$ipv4->ipv4_address]:64610->[192.168.5.5]:162
DISMAN-EVENT-MIB::sysUpTimeInstance 198:2:10:48.91\n";
$trap = new Trap($trapText);
$this->assertFalse($trap->handle(), 'Found handler for trap with no snmpTrapOID');
$this->assertFalse(Dispatcher::handle($trap), 'Found handler for trap with no snmpTrapOID');
// check that the device was found
$this->assertEquals($device->hostname, $trap->getDevice()->hostname);
@@ -73,7 +74,7 @@ DISMAN-EVENT-MIB::sysUpTimeInstance 198:2:10:48.91
SNMPv2-MIB::snmpTrapOID.0 SNMPv2-MIB::authenticationFailure\n";
$trap = new Trap($trapText);
$this->assertTrue($trap->handle());
$this->assertTrue(Dispatcher::handle($trap));
// check that the device was found
$this->assertEquals($device->hostname, $trap->getDevice()->hostname);
@@ -97,7 +98,7 @@ BGP4-MIB::bgpPeerLastError.$bgppeer->bgpPeerIdentifier \"04 00 \"
BGP4-MIB::bgpPeerState.$bgppeer->bgpPeerIdentifier established\n";
$trap = new Trap($trapText);
$this->assertTrue($trap->handle(), 'Could not handle bgpEstablished');
$this->assertTrue(Dispatcher::handle($trap), 'Could not handle bgpEstablished');
$bgppeer = $bgppeer->fresh(); // refresh from database
$this->assertEquals($bgppeer->bgpPeerState, 'established');
@@ -117,7 +118,7 @@ BGP4-MIB::bgpPeerLastError.$bgppeer->bgpPeerIdentifier \"04 00 \"
BGP4-MIB::bgpPeerState.$bgppeer->bgpPeerIdentifier idle\n";
$trap = new Trap($trapText);
$this->assertTrue($trap->handle(), 'Could not handle bgpBackwardTransition');
$this->assertTrue(Dispatcher::handle($trap), 'Could not handle bgpBackwardTransition');
$bgppeer = $bgppeer->fresh(); // refresh from database
$this->assertEquals($bgppeer->bgpPeerState, 'idle');
@@ -142,7 +143,7 @@ IF-MIB::ifType.$port->ifIndex ethernetCsmacd
OLD-CISCO-INTERFACES-MIB::locIfReason.$port->ifIndex \"down\"\n";
$trap = new Trap($trapText);
$this->assertTrue($trap->handle(), 'Could not handle linkDown');
$this->assertTrue(Dispatcher::handle($trap), 'Could not handle linkDown');
$port = $port->fresh(); // refresh from database
@@ -169,7 +170,7 @@ IF-MIB::ifType.$port->ifIndex ethernetCsmacd
OLD-CISCO-INTERFACES-MIB::locIfReason.$port->ifIndex \"up\"\n";
$trap = new Trap($trapText);
$this->assertTrue($trap->handle(), 'Could not handle linkUp');
$this->assertTrue(Dispatcher::handle($trap), 'Could not handle linkUp');
$port = $port->fresh(); // refresh from database
$this->assertEquals($port->ifAdminStatus, 'up');

105
tests/Unit/DeviceTest.php Normal file
View File

@@ -0,0 +1,105 @@
<?php
/**
* DeviceTest.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 LibreNMS\Tests\Unit;
use App\Models\Device;
use App\Models\Ipv4Address;
use App\Models\Port;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use LibreNMS\Tests\LaravelTestCase;
class DeviceTest extends LaravelTestCase
{
use DatabaseTransactions;
public function testFindByHostname()
{
$device = factory(Device::class)->create();
$found = Device::findByHostname($device->hostname);
$this->assertNotNull($found);
$this->assertEquals($device->device_id, $found->device_id, "Did not find the correct device");
}
public function testFindByIpFail()
{
$found = Device::findByIp('this is not an ip');
$this->assertNull($found);
}
public function testFindByIpv4Fail()
{
$found = Device::findByIp('182.43.219.43');
$this->assertNull($found);
}
public function testFindByIpv6Fail()
{
$found = Device::findByIp('341a:234d:3429:9845:909f:fd32:1930:32dc');
$this->assertNull($found);
}
public function testFindIpButNoPort()
{
$ipv4 = factory(Ipv4Address::class)->create();
Port::destroy($ipv4->port_id);
$found = Device::findByIp($ipv4->ipv4_address);
$this->assertNull($found);
}
public function testFindByIp()
{
$device = factory(Device::class)->create();
$found = Device::findByIp($device->ip);
$this->assertNotNull($found);
$this->assertEquals($device->device_id, $found->device_id, "Did not find the correct device");
}
public function testFindByIpHostname()
{
$ip = '192.168.234.32';
$device = factory(Device::class)->create(['hostname' => $ip]);
$found = Device::findByIp($ip);
$this->assertNotNull($found);
$this->assertEquals($device->device_id, $found->device_id, "Did not find the correct device");
}
public function testFindByIpThroughPort()
{
$device = factory(Device::class)->create();
$port = factory(Port::class)->make();
$device->ports()->save($port);
$ipv4 = factory(Ipv4Address::class)->make(); // test ipv4 lookup of device
$port->ipv4()->save($ipv4);
$found = Device::findByIp($ipv4->ipv4_address);
$this->assertNotNull($found);
$this->assertEquals($device->device_id, $found->device_id, "Did not find the correct device");
}
}