2020-04-11 20:29:13 +01:00
<? php
/* Copyright (C) 2020 Adam Bishop <adam@omega.org.uk>
* 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
* 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/>. */
/**
* API Transport
* @author Adam Bishop <adam@omega.org.uk>
* @copyright 2020 Adam Bishop, LibreNMS
* @license GPL
* @package LibreNMS
* @subpackage Alerts
*/
2020-09-21 14:54:51 +02:00
namespace LibreNMS\Alert\Transport ;
2020-04-11 20:29:13 +01:00
use GuzzleHttp\Client ;
use GuzzleHttp\Exception\GuzzleException ;
use Illuminate\Support\Facades\Log ;
2020-09-21 14:54:51 +02:00
use LibreNMS\Alert\Transport ;
use LibreNMS\Config ;
use LibreNMS\Enum\AlertState ;
2020-04-11 20:29:13 +01:00
class Sensu extends Transport
{
// Sensu alert coding
const OK = 0 ;
const WARNING = 1 ;
const CRITICAL = 2 ;
const UNKNOWN = 3 ;
2020-09-21 14:54:51 +02:00
private static $status = [
2020-04-11 20:29:13 +01:00
'ok' => Sensu :: OK ,
'warning' => Sensu :: WARNING ,
2020-09-21 14:54:51 +02:00
'critical' => Sensu :: CRITICAL ,
];
2020-04-11 20:29:13 +01:00
2020-09-21 14:54:51 +02:00
private static $severity = [
2020-05-24 04:14:36 +02:00
'recovered' => AlertState :: RECOVERED ,
'alert' => AlertState :: ACTIVE ,
'acknowledged' => AlertState :: ACKNOWLEDGED ,
'worse' => AlertState :: WORSE ,
'better' => AlertState :: BETTER ,
2020-09-21 14:54:51 +02:00
];
2020-04-11 20:29:13 +01:00
private static $client = null ;
public function deliverAlert ( $obj , $opts )
{
$sensu_opts = [];
$sensu_opts [ 'url' ] = $this -> config [ 'sensu-url' ] ? $this -> config [ 'sensu-url' ] : 'http://127.0.0.1:3031' ;
2020-09-21 14:54:51 +02:00
$sensu_opts [ 'namespace' ] = $this -> config [ 'sensu-namespace' ] ? $this -> config [ 'sensu-namespace' ] : 'default' ;
$sensu_opts [ 'prefix' ] = $this -> config [ 'sensu-prefix' ];
2020-04-11 20:29:13 +01:00
$sensu_opts [ 'source-key' ] = $this -> config [ 'sensu-source-key' ];
Sensu :: $client = new Client ();
try {
return $this -> contactSensu ( $obj , $sensu_opts );
} catch ( GuzzleException $e ) {
return "Sending event to Sensu failed: " . $e -> getMessage ();
}
}
public static function contactSensu ( $obj , $opts )
{
// The Sensu agent should be running on the poller - events can be sent directly to the backend but this has not been tested, and likely needs mTLS.
// The agent API is documented at https://docs.sensu.io/sensu-go/latest/reference/agent/#create-monitoring-events-using-the-agent-api
if ( Sensu :: $client -> request ( 'GET' , $opts [ 'url' ] . '/healthz' ) -> getStatusCode () !== 200 ) {
return 'Sensu API is not responding' ;
}
2020-05-24 04:14:36 +02:00
if ( $obj [ 'state' ] !== AlertState :: RECOVERED && $obj [ 'state' ] !== AlertState :: ACKNOWLEDGED && $obj [ 'alerted' ] === 0 ) {
2020-04-11 20:29:13 +01:00
// If this is the first event, send a forced "ok" dated (rrd.step / 2) seconds ago to tell Sensu the last time the check was healthy
$data = Sensu :: generateData ( $obj , $opts , Sensu :: OK , round ( Config :: get ( 'rrd.step' , 300 ) / 2 ));
Log :: debug ( 'Sensu transport sent last good event to socket: ' , $data );
$result = Sensu :: $client -> request ( 'POST' , $opts [ 'url' ] . '/events' , [ 'json' => $data ]);
if ( $result -> getStatusCode () !== 202 ) {
return $result -> getReasonPhrase ();
}
sleep ( 5 );
}
$data = Sensu :: generateData ( $obj , $opts , Sensu :: calculateStatus ( $obj [ 'state' ], $obj [ 'severity' ]));
Log :: debug ( 'Sensu transport sent event to socket: ' , $data );
$result = Sensu :: $client -> request ( 'POST' , $opts [ 'url' ] . '/events' , [ 'json' => $data ]);
if ( $result -> getStatusCode () === 202 ) {
return true ;
}
return $result -> getReasonPhrase ();
}
public static function generateData ( $obj , $opts , $status , $offset = 0 )
{
return [
'check' => [
'metadata' => [
'name' => Sensu :: checkName ( $opts [ 'prefix' ], $obj [ 'name' ]),
'namespace' => $opts [ 'namespace' ],
'annotations' => Sensu :: generateAnnotations ( $obj ),
],
'command' => sprintf ( 'LibreNMS: %s' , $obj [ 'builder' ]),
'executed' => time () - $offset ,
'interval' => Config :: get ( 'rrd.step' , 300 ),
'issued' => time () - $offset ,
'output' => $obj [ 'msg' ],
'status' => $status ,
],
'entity' => [
'metadata' => [
'name' => Sensu :: getEntityName ( $obj , $opts [ 'source-key' ]),
'namespace' => $opts [ 'namespace' ],
],
'system' => [
'hostname' => $obj [ 'hostname' ],
'os' => $obj [ 'os' ],
2020-09-21 14:54:51 +02:00
],
2020-04-11 20:29:13 +01:00
],
];
}
public static function generateAnnotations ( $obj )
{
return array_filter ([
'generated-by' => 'LibreNMS' ,
2020-05-24 04:14:36 +02:00
'acknowledged' => $obj [ 'state' ] === AlertState :: ACKNOWLEDGED ? 'true' : 'false' ,
2020-04-11 20:29:13 +01:00
'contact' => $obj [ 'sysContact' ],
'description' => $obj [ 'sysDescr' ],
'location' => $obj [ 'location' ],
'documentation' => $obj [ 'proc' ],
'librenms-notes' => $obj [ 'notes' ],
'librenms-device-id' => strval ( $obj [ 'device_id' ]),
'librenms-rule-id' => strval ( $obj [ 'rule_id' ]),
'librenms-status-reason' => $obj [ 'status_reason' ],
], 'strlen' ); // strlen returns 0 for null, false or '', but 1 for integer 0 - unlike empty()
}
public static function calculateStatus ( $state , $severity )
{
// Sensu only has a single short (status) to indicate both severity and status, so we need to map LibreNMS' state and severity onto it
2020-05-24 04:14:36 +02:00
if ( $state === AlertState :: RECOVERED ) {
2020-04-11 20:29:13 +01:00
// LibreNMS alert is resolved, send ok
return Sensu :: OK ;
}
if ( array_key_exists ( $severity , Sensu :: $status )) {
// Severity is known, map the LibreNMS severity to the Sensu status
return Sensu :: $status [ $severity ];
}
// LibreNMS severity does not map to Sensu, send unknown
return Sensu :: UNKNOWN ;
}
public static function getEntityName ( $obj , $key )
{
if ( $key === 'shortname' ) {
return Sensu :: shortenName ( $obj [ 'hostname' ]);
}
return $obj [ $key ];
}
public static function shortenName ( $name )
{
// Shrink the last domain components - e.g. librenms.corp.example.net becomes librenms.cen
$components = explode ( '.' , $name );
$count = count ( $components );
$trim = min ([ 3 , $count - 1 ]);
$result = '' ;
if ( $count <= 2 ) { // Can't be shortened
return $name ;
}
for ( $i = $count - 1 ; $i >= $count - $trim ; $i -- ) {
// Walk the array in reverse order, taking the first letter from the $trim sections
$result = sprintf ( '%s%s' , substr ( $components [ $i ], 0 , 1 ), $result );
unset ( $components [ $i ]);
}
return sprintf ( '%s.%s' , implode ( '.' , $components ), $result );
}
public static function checkName ( $prefix , $name )
{
$check = strtolower ( str_replace ( ' ' , '-' , $name ));
if ( $prefix ) {
return sprintf ( '%s-%s' , $prefix , $check );
}
return $check ;
}
public static function configTemplate ()
{
return [
'config' => [
[
'title' => 'Sensu Endpoint' ,
'name' => 'sensu-url' ,
'descr' => 'To configure the agent API, see https://docs.sensu.io/sensu-go/latest/reference/agent/#api-configuration-flags (default: "http://localhost:3031")' ,
'type' => 'text' ,
],
[
'title' => 'Sensu Namespace' ,
'name' => 'sensu-namespace' ,
'descr' => 'The Sensu namespace that hosts exist in (default: "default")' ,
'type' => 'text' ,
],
[
'title' => 'Check Prefix' ,
'name' => 'sensu-prefix' ,
'descr' => 'An optional string to prefix the checks with' ,
'type' => 'text' ,
],
[
'title' => 'Source Key' ,
'name' => 'sensu-source-key' ,
'descr' => 'Should events be attributed to entities by hostname, sysName or shortname (default: hostname)' ,
'type' => 'select' ,
'options' => [
'hostname' => 'hostname' ,
'sysName' => 'sysName' ,
2020-09-21 14:54:51 +02:00
'shortname' => 'shortname' ,
2020-04-11 20:29:13 +01:00
],
2020-09-21 14:54:51 +02:00
'default' => 'hostname' ,
2020-04-11 20:29:13 +01:00
],
],
'validation' => [
'sensu-url' => 'url' ,
'sensu-source-key' => 'required|in:hostname,sysName,shortname' ,
2020-09-21 14:54:51 +02:00
],
2020-04-11 20:29:13 +01:00
];
}
}