diff --git a/LibreNMS/Snmptrap/Dispatcher.php b/LibreNMS/Snmptrap/Dispatcher.php new file mode 100644 index 0000000000..b988f1c36b --- /dev/null +++ b/LibreNMS/Snmptrap/Dispatcher.php @@ -0,0 +1,59 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2018 Tony Murray + * @author Tony Murray + */ + +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; + } +} diff --git a/LibreNMS/Snmptrap/Handler/AuthenticationFailure.php b/LibreNMS/Snmptrap/Handlers/AuthenticationFailure.php similarity index 97% rename from LibreNMS/Snmptrap/Handler/AuthenticationFailure.php rename to LibreNMS/Snmptrap/Handlers/AuthenticationFailure.php index e9f52680c8..d342061fac 100644 --- a/LibreNMS/Snmptrap/Handler/AuthenticationFailure.php +++ b/LibreNMS/Snmptrap/Handlers/AuthenticationFailure.php @@ -23,7 +23,7 @@ * @author Tony Murray */ -namespace LibreNMS\Snmptrap\Handler; +namespace LibreNMS\Snmptrap\Handlers; use App\Models\Device; use LibreNMS\Interfaces\SnmptrapHandler; diff --git a/LibreNMS/Snmptrap/Handler/BgpBackwardTransition.php b/LibreNMS/Snmptrap/Handlers/BgpBackwardTransition.php similarity index 98% rename from LibreNMS/Snmptrap/Handler/BgpBackwardTransition.php rename to LibreNMS/Snmptrap/Handlers/BgpBackwardTransition.php index 092bccdf2d..7db2cebe27 100644 --- a/LibreNMS/Snmptrap/Handler/BgpBackwardTransition.php +++ b/LibreNMS/Snmptrap/Handlers/BgpBackwardTransition.php @@ -23,7 +23,7 @@ * @author Tony Murray */ -namespace LibreNMS\Snmptrap\Handler; +namespace LibreNMS\Snmptrap\Handlers; use App\Models\Device; use LibreNMS\Interfaces\SnmptrapHandler; diff --git a/LibreNMS/Snmptrap/Handler/BgpEstablished.php b/LibreNMS/Snmptrap/Handlers/BgpEstablished.php similarity index 98% rename from LibreNMS/Snmptrap/Handler/BgpEstablished.php rename to LibreNMS/Snmptrap/Handlers/BgpEstablished.php index 29d139e7bd..b092cebb13 100644 --- a/LibreNMS/Snmptrap/Handler/BgpEstablished.php +++ b/LibreNMS/Snmptrap/Handlers/BgpEstablished.php @@ -23,7 +23,7 @@ * @author Tony Murray */ -namespace LibreNMS\Snmptrap\Handler; +namespace LibreNMS\Snmptrap\Handlers; use App\Models\Device; use LibreNMS\Interfaces\SnmptrapHandler; diff --git a/LibreNMS/Snmptrap/Handler/Fallback.php b/LibreNMS/Snmptrap/Handlers/Fallback.php similarity index 97% rename from LibreNMS/Snmptrap/Handler/Fallback.php rename to LibreNMS/Snmptrap/Handlers/Fallback.php index 3d5e81a5f5..551db4a4b3 100644 --- a/LibreNMS/Snmptrap/Handler/Fallback.php +++ b/LibreNMS/Snmptrap/Handlers/Fallback.php @@ -23,7 +23,7 @@ * @author Tony Murray */ -namespace LibreNMS\Snmptrap\Handler; +namespace LibreNMS\Snmptrap\Handlers; use App\Models\Device; use LibreNMS\Interfaces\SnmptrapHandler; diff --git a/LibreNMS/Snmptrap/Handler/LinkDown.php b/LibreNMS/Snmptrap/Handlers/LinkDown.php similarity index 98% rename from LibreNMS/Snmptrap/Handler/LinkDown.php rename to LibreNMS/Snmptrap/Handlers/LinkDown.php index 50237bcd1d..ef04277c0b 100644 --- a/LibreNMS/Snmptrap/Handler/LinkDown.php +++ b/LibreNMS/Snmptrap/Handlers/LinkDown.php @@ -23,7 +23,7 @@ * @author Tony Murray */ -namespace LibreNMS\Snmptrap\Handler; +namespace LibreNMS\Snmptrap\Handlers; use App\Models\Device; use LibreNMS\Interfaces\SnmptrapHandler; diff --git a/LibreNMS/Snmptrap/Handler/LinkUp.php b/LibreNMS/Snmptrap/Handlers/LinkUp.php similarity index 98% rename from LibreNMS/Snmptrap/Handler/LinkUp.php rename to LibreNMS/Snmptrap/Handlers/LinkUp.php index 555d3df60e..10d3dcdabc 100644 --- a/LibreNMS/Snmptrap/Handler/LinkUp.php +++ b/LibreNMS/Snmptrap/Handlers/LinkUp.php @@ -23,7 +23,7 @@ * @author Tony Murray */ -namespace LibreNMS\Snmptrap\Handler; +namespace LibreNMS\Snmptrap\Handlers; use App\Models\Device; use LibreNMS\Interfaces\SnmptrapHandler; diff --git a/LibreNMS/Snmptrap/Trap.php b/LibreNMS/Snmptrap/Trap.php index bccb1d0cc5..54b997e539 100644 --- a/LibreNMS/Snmptrap/Trap.php +++ b/LibreNMS/Snmptrap/Trap.php @@ -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 * diff --git a/LibreNMS/Util/CliColorFormatter.php b/LibreNMS/Util/CliColorFormatter.php index 35be8f553d..f3c24d0ee5 100644 --- a/LibreNMS/Util/CliColorFormatter.php +++ b/LibreNMS/Util/CliColorFormatter.php @@ -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); diff --git a/app/Models/Device.php b/app/Models/Device.php index 5d60a1a182..92d9b0fe8d 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -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) { diff --git a/app/Providers/SnmptrapProvider.php b/app/Providers/SnmptrapProvider.php index 93c3d5f2a7..b3034db0ed 100644 --- a/app/Providers/SnmptrapProvider.php +++ b/app/Providers/SnmptrapProvider.php @@ -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 { diff --git a/config/snmptraps.php b/config/snmptraps.php index d060baa6d1..6f6e0f3a23 100644 --- a/config/snmptraps.php +++ b/config/snmptraps.php @@ -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, ] ]; diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 1c82a53f1e..adc041dda1 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -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; - } + }, ]; }); diff --git a/doc/Developing/SNMP-Traps.md b/doc/Developing/SNMP-Traps.md new file mode 100644 index 0000000000..2474b1191f --- /dev/null +++ b/doc/Developing/SNMP-Traps.md @@ -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(); +``` diff --git a/doc/Extensions/SNMP-Trap-Handler.md b/doc/Extensions/SNMP-Trap-Handler.md index ef7072b9a8..8257465c5f 100644 --- a/doc/Extensions/SNMP-Trap-Handler.md +++ b/doc/Extensions/SNMP-Trap-Handler.md @@ -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) diff --git a/includes/defaults.inc.php b/includes/defaults.inc.php index 3858c59266..6d1c62cfa4 100644 --- a/includes/defaults.inc.php +++ b/includes/defaults.inc.php @@ -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'; diff --git a/mkdocs.yml b/mkdocs.yml index 9af25539fa..aaa715d2ec 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/snmptrap.php b/snmptrap.php index be9f021fec..cb6a9002cf 100755 --- a/snmptrap.php +++ b/snmptrap.php @@ -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)); diff --git a/tests/SnmpTrapTest.php b/tests/SnmpTrapTest.php index 7d94a30523..853c1872d7 100644 --- a/tests/SnmpTrapTest.php +++ b/tests/SnmpTrapTest.php @@ -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'); diff --git a/tests/Unit/DeviceTest.php b/tests/Unit/DeviceTest.php new file mode 100644 index 0000000000..934acfc451 --- /dev/null +++ b/tests/Unit/DeviceTest.php @@ -0,0 +1,105 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2018 Tony Murray + * @author Tony Murray + */ + +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"); + } +}