Alert transport cleanup, no_proxy support and other proxy cleanups (#14763)

* Add no_proxy and other proxy related settings
Set user agent on all http client requests
Unify http client usage

* Style fixes

* Remove useless use statements

* Correct variable, good job phpstan

* Add tests
fix https_proxy bug
add tcp:// to the config settings format

* style and lint fixes

* Remove guzzle from the direct dependencies

* Use built in Laravel testing functionality

* update baseline
This commit is contained in:
Tony Murray 2023-05-23 09:25:17 -05:00 committed by GitHub
parent 02896172bd
commit 04bb75f5f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 1545 additions and 2314 deletions

View File

@ -25,6 +25,11 @@
namespace LibreNMS\Alert; namespace LibreNMS\Alert;
use App\Models\Device;
use LibreNMS\Config;
use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Time;
class AlertData extends \Illuminate\Support\Collection class AlertData extends \Illuminate\Support\Collection
{ {
public function __get($name) public function __get($name)
@ -35,4 +40,50 @@ class AlertData extends \Illuminate\Support\Collection
return "$name is not a valid \$alert data name"; return "$name is not a valid \$alert data name";
} }
public static function testData(Device $device, array $faults = []): array
{
return [
'hostname' => $device->hostname,
'device_id' => $device->device_id,
'sysDescr' => $device->sysDescr,
'sysName' => $device->sysName,
'sysContact' => $device->sysContact,
'os' => $device->os,
'type' => $device->type,
'ip' => $device->ip,
'display' => $device->displayName(),
'version' => $device->version,
'hardware' => $device->hardware,
'features' => $device->features,
'serial' => $device->serial,
'status' => $device->status,
'status_reason' => $device->status_reason,
'location' => (string) $device->location,
'description' => $device->purpose,
'notes' => $device->notes,
'uptime' => $device->uptime,
'uptime_short' => Time::formatInterval($device->uptime, true),
'uptime_long' => Time::formatInterval($device->uptime),
'title' => 'Testing transport from ' . Config::get('project_name'),
'elapsed' => '11s',
'alerted' => 0,
'alert_id' => '000',
'alert_notes' => 'This is the note for the test alert',
'proc' => 'This is the procedure for the test alert',
'rule_id' => '000',
'id' => '000',
'faults' => $faults,
'uid' => '000',
'severity' => 'critical',
'rule' => 'macros.device = 1',
'name' => 'Test-Rule',
'string' => '#1: test => string;',
'timestamp' => date('Y-m-d H:i:s'),
'contacts' => AlertUtil::getContacts($device->toArray()),
'state' => AlertState::ACTIVE,
'msg' => 'This is a test alert',
'builder' => '{}',
];
}
} }

View File

@ -4,7 +4,6 @@ namespace LibreNMS\Alert;
use App\Models\AlertTransport; use App\Models\AlertTransport;
use App\View\SimpleTemplate; use App\View\SimpleTemplate;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use LibreNMS\Config; use LibreNMS\Config;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
@ -12,11 +11,8 @@ use LibreNMS\Interfaces\Alert\Transport as TransportInterface;
abstract class Transport implements TransportInterface abstract class Transport implements TransportInterface
{ {
protected $config; protected ?array $config;
/** protected string $name = '';
* @var string
*/
protected $name;
public static function make(string $type): TransportInterface public static function make(string $type): TransportInterface
{ {
@ -43,25 +39,9 @@ abstract class Transport implements TransportInterface
return $list; return $list;
} }
/** public function __construct(?AlertTransport $transport = null)
* Transport constructor.
*
* @param null $transport
*/
public function __construct($transport = null)
{ {
if (! empty($transport)) { $this->config = $transport ? $transport->transport_config : [];
if ($transport instanceof AlertTransport) {
$this->config = $transport->transport_config;
} else {
try {
$model = \App\Models\AlertTransport::findOrFail($transport); /** @var AlertTransport $model */
$this->config = $model->transport_config;
} catch (ModelNotFoundException $e) {
$this->config = [];
}
}
}
} }
/** /**
@ -69,7 +49,7 @@ abstract class Transport implements TransportInterface
*/ */
public function name(): string public function name(): string
{ {
if ($this->name !== null) { if ($this->name !== '') {
return $this->name; return $this->name;
} }
@ -158,7 +138,7 @@ abstract class Transport implements TransportInterface
return 'LibreNMS\\Alert\\Transport\\' . ucfirst($type); return 'LibreNMS\\Alert\\Transport\\' . ucfirst($type);
} }
protected function isHtmlContent($content): bool protected function isHtmlContent(string $content): bool
{ {
return $content !== strip_tags($content); return $content !== strip_tags($content);
} }

View File

@ -5,6 +5,7 @@
* Free Software Foundation, either version 3 of the License, or (at your * Free Software Foundation, either version 3 of the License, or (at your
* option) any later version. Please see LICENSE.txt at the top level of * option) any later version. Please see LICENSE.txt at the top level of
* the source code distribution for details. */ * the source code distribution for details. */
/** /**
* API Transport * API Transport
* *
@ -16,76 +17,53 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Config;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
use LibreNMS\Util\Url;
class Alerta extends Transport class Alerta extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$opts['url'] = $this->config['alerta-url']; $severity = ($alert_data['state'] == AlertState::RECOVERED ? $this->config['recoverstate'] : $this->config['alertstate']);
$opts['environment'] = $this->config['environment'];
$opts['apikey'] = $this->config['apikey'];
$opts['alertstate'] = $this->config['alertstate'];
$opts['recoverstate'] = $this->config['recoverstate'];
return $this->contactAlerta($obj, $opts);
}
public function contactAlerta($obj, $opts)
{
$host = $opts['url'];
$curl = curl_init();
$text = strip_tags($obj['msg']);
$severity = ($obj['state'] == AlertState::RECOVERED ? $opts['recoverstate'] : $opts['alertstate']);
$deviceurl = (Config::get('base_url') . 'device/device=' . $obj['device_id']);
$devicehostname = $obj['hostname'];
$data = [ $data = [
'resource' => $devicehostname, 'resource' => $alert_data['display'],
'event' => $obj['name'], 'event' => $alert_data['name'],
'environment' => $opts['environment'], 'environment' => $this->config['environment'],
'severity' => $severity, 'severity' => $severity,
'service' => [$obj['title']], 'service' => [$alert_data['title']],
'group' => $obj['name'], 'group' => $alert_data['name'],
'value' => $obj['state'], 'value' => $alert_data['state'],
'text' => $text, 'text' => strip_tags($alert_data['msg']),
'tags' => [$obj['title']], 'tags' => [$alert_data['title']],
'attributes' => [ 'attributes' => [
'sysName' => $obj['sysName'], 'sysName' => $alert_data['sysName'],
'sysDescr' => $obj['sysDescr'], 'sysDescr' => $alert_data['sysDescr'],
'os' => $obj['os'], 'os' => $alert_data['os'],
'type' => $obj['type'], 'type' => $alert_data['type'],
'ip' => $obj['ip'], 'ip' => $alert_data['ip'],
'uptime' => $obj['uptime_long'], 'uptime' => $alert_data['uptime_long'],
'moreInfo' => '<a href=' . $deviceurl . '>' . $devicehostname . '</a>', 'moreInfo' => '<a href=' . Url::deviceUrl($alert_data['device_id']) . '>' . $alert_data['display'] . '</a>',
], ],
'origin' => $obj['rule'], 'origin' => $alert_data['rule'],
'type' => $obj['title'], 'type' => $alert_data['title'],
]; ];
$alert_message = json_encode($data);
Proxy::applyToCurl($curl);
$headers = ['Content-Type: application/json', 'Authorization: Key ' . $opts['apikey']];
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_URL, $host);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $alert_message);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 201) {
var_dump("API '$host' returned Error");
var_dump('Params: ' . $alert_message);
var_dump('Return: ' . $ret);
var_dump('Headers: ', $headers);
return 'HTTP Status code ' . $code; $res = Http::client()
->withHeaders([
'Authorization' => 'Key ' . $this->config['apikey'],
])
->post($this->config['alerta-url'], $data);
if ($res->successful()) {
return true;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $data['text'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -23,116 +23,67 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Config;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
use LibreNMS\Util\Url;
class Alertmanager extends Transport class Alertmanager extends Transport
{ {
protected $name = 'Alert Manager'; protected string $name = 'Alert Manager';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$alertmanager_opts = $this->parseUserOptions($this->config['alertmanager-options']); $url = $this->config['alertmanager-url'];
$alertmanager_opts['url'] = $this->config['alertmanager-url']; $username = $this->config['alertmanager-username'];
$password = $this->config['alertmanager-password'];
$alertmanager_username = $this->config['alertmanager-username']; $alertmanager_status = $alert_data['state'] == AlertState::RECOVERED ? 'endsAt' : 'startsAt';
$alertmanager_password = $this->config['alertmanager-password']; $alertmanager_msg = strip_tags($alert_data['msg']);
return $this->contactAlertmanager($obj, $alertmanager_opts, $alertmanager_username, $alertmanager_password);
}
public function contactAlertmanager($obj, $api, string $username, string $password)
{
if ($obj['state'] == AlertState::RECOVERED) {
$alertmanager_status = 'endsAt';
} else {
$alertmanager_status = 'startsAt';
}
$gen_url = (Config::get('base_url') . 'device/device=' . $obj['device_id']);
$alertmanager_msg = strip_tags($obj['msg']);
$data = [[ $data = [[
$alertmanager_status => date('c'), $alertmanager_status => date('c'),
'generatorURL' => $gen_url, 'generatorURL' => Url::deviceUrl($alert_data['device_id']),
'annotations' => [ 'annotations' => [
'summary' => $obj['name'], 'summary' => $alert_data['name'],
'title' => $obj['title'], 'title' => $alert_data['title'],
'description' => $alertmanager_msg, 'description' => $alertmanager_msg,
], ],
'labels' => [ 'labels' => [
'alertname' => $obj['name'], 'alertname' => $alert_data['name'],
'severity' => $obj['severity'], 'severity' => $alert_data['severity'],
'instance' => $obj['hostname'], 'instance' => $alert_data['hostname'],
], ],
]]; ]];
$url = $api['url']; $alertmanager_opts = $this->parseUserOptions($this->config['alertmanager-options']);
unset($api['url']); foreach ($alertmanager_opts as $label => $value) {
foreach ($api as $label => $value) {
// To allow dynamic values // To allow dynamic values
if (preg_match('/^extra_[A-Za-z0-9_]+$/', $label) && (! empty($obj['faults'][1][$value]))) { if (preg_match('/^extra_[A-Za-z0-9_]+$/', $label) && ! empty($alert_data['faults'][1][$value])) {
$data[0]['labels'][$label] = $obj['faults'][1][$value]; $data[0]['labels'][$label] = $alert_data['faults'][1][$value];
} else { } else {
$data[0]['labels'][$label] = $value; $data[0]['labels'][$label] = $value;
} }
} }
return $this->postAlerts($url, $data, $username, $password); $client = Http::client()->timeout(5);
}
public static function postAlerts($url, $data, string $username, string $password)
{
$curl = curl_init();
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_TIMEOUT, 5);
curl_setopt($curl, CURLOPT_TIMEOUT_MS, 5000);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT_MS, 5000);
curl_setopt($curl, CURLOPT_POST, true);
if ($username != '' && $password != '') { if ($username != '' && $password != '') {
curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); $client->withBasicAuth($username, $password);
curl_setopt($curl, CURLOPT_USERNAME, $username);
curl_setopt($curl, CURLOPT_PASSWORD, $password);
} }
$alert_message = json_encode($data);
curl_setopt($curl, CURLOPT_POSTFIELDS, $alert_message);
foreach (explode(',', $url) as $am) { foreach (explode(',', $url) as $am) {
$post_url = ($am . '/api/v2/alerts'); $post_url = ($am . '/api/v2/alerts');
curl_setopt($curl, CURLOPT_URL, $post_url); $res = $client->post($post_url, $data);
$ret = curl_exec($curl);
if ($ret === false || curl_errno($curl)) {
logfile("Failed to contact $post_url: " . curl_error($curl));
continue;
}
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code == 200) {
curl_close($curl);
if ($res->successful()) {
return true; return true;
} }
} }
$err = "Unable to POST to Alertmanager at $post_url ."; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $alertmanager_msg, $data);
if ($ret === false || curl_errno($curl)) {
$err .= ' cURL error: ' . curl_error($curl);
} else {
$err .= ' HTTP status: ' . curl_getinfo($curl, CURLINFO_HTTP_CODE);
}
curl_close($curl);
logfile($err);
return $err;
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -26,73 +26,57 @@ namespace LibreNMS\Alert\Transport;
use App\View\SimpleTemplate; use App\View\SimpleTemplate;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Api extends Transport class Api extends Transport
{ {
protected $name = 'API'; protected string $name = 'API';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$url = $this->config['api-url']; $request_body = $this->config['api-body'];
$options = $this->config['api-options']; $username = $this->config['api-auth-username'];
$headers = $this->config['api-headers']; $password = $this->config['api-auth-password'];
$body = $this->config['api-body'];
$method = $this->config['api-method'];
$auth = [$this->config['api-auth-username'], $this->config['api-auth-password']];
return $this->contactAPI($obj, $url, $options, $method, $auth, $headers, $body); $method = strtolower($this->config['api-method']);
$host = explode('?', $this->config['api-url'], 2)[0]; //we don't use the parameter part, cause we build it out of options.
//get each line of key-values and process the variables for Options
$query = $this->parseUserOptions($this->config['api-options'], $alert_data);
$request_headers = $this->parseUserOptions($this->config['api-headers'], $alert_data);
$client = Http::client()
->withHeaders($request_headers); //get each line of key-values and process the variables for Headers
if ($method !== 'get') {
$request_body = SimpleTemplate::parse($this->config['api-body'], $alert_data);
$client->withBody($request_body, 'text/plain'); // Content-Type can be overriden by user headers
}
if ($username) {
$client->withBasicAuth($username, $password);
}
$client->withOptions([
'query' => $query,
]);
$res = match ($method) {
'get' => $client->get($host),
'put' => $client->put($host),
default => $client->post($host),
};
if ($res->successful()) {
return true;
}
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $request_body ?? $query, [
'query' => $query,
]);
} }
private function contactAPI($obj, $api, $options, $method, $auth, $headers, $body) public static function configTemplate(): array
{
$request_opts = [];
$method = strtolower($method);
$host = explode('?', $api, 2)[0]; //we don't use the parameter part, cause we build it out of options.
//get each line of key-values and process the variables for Headers;
$request_heads = $this->parseUserOptions($headers, $obj);
//get each line of key-values and process the variables for Options;
$query = $this->parseUserOptions($options, $obj);
$client = app(\GuzzleHttp\Client::class);
$request_opts['proxy'] = Proxy::forGuzzle();
if (isset($auth) && ! empty($auth[0])) {
$request_opts['auth'] = $auth;
}
if (count($request_heads) > 0) {
$request_opts['headers'] = $request_heads;
}
if ($method == 'get') {
$request_opts['query'] = $query;
$res = $client->request('GET', $host, $request_opts);
} elseif ($method == 'put') {
$request_opts['query'] = $query;
$request_opts['body'] = $body;
$res = $client->request('PUT', $host, $request_opts);
} else { //Method POST
$request_opts['query'] = $query;
$request_opts['body'] = SimpleTemplate::parse($body, $obj);
$res = $client->request('POST', $host, $request_opts);
}
$code = $res->getStatusCode();
if ($code != 200) {
var_dump("API '$host' returned Error");
var_dump('Params:');
var_dump($query);
var_dump('Response headers:');
var_dump($res->getHeaders());
var_dump('Return: ' . $res->getReasonPhrase());
return 'HTTP Status code ' . $code;
}
return true;
}
public static function configTemplate()
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -1,142 +0,0 @@
<?php
/* Copyright (C) 2015 James Campbell <neokjames@gmail.com>
* 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 <https://www.gnu.org/licenses/>. */
/* Copyright (C) 2015 Daniel Preussker <f0o@devilcode.org>
* 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 <https://www.gnu.org/licenses/>. */
/**
* Boxcar API Transport
*
* @author trick77 <jan@trick77.com>
* @copyright 2015 trick77, neokjames, f0o, LibreNMS
* @license GPL
*/
namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport;
use LibreNMS\Config;
use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Proxy;
class Boxcar extends Transport
{
public function deliverAlert($obj, $opts)
{
$boxcar_opts = $this->parseUserOptions($this->config['options']);
$boxcar_opts['access_token'] = $this->config['boxcar-token'];
return $this->contactBoxcar($obj, $boxcar_opts);
}
public static function contactBoxcar($obj, $api)
{
$data = [];
$data['user_credentials'] = $api['access_token'];
$data['notification[source_name]'] = Config::get('project_id', 'librenms');
switch ($obj['severity']) {
case 'critical':
$severity = 'Critical';
if (! empty($api['sound_critical'])) {
$data['notification[sound]'] = $api['sound_critical'];
}
break;
case 'warning':
$severity = 'Warning';
if (! empty($api['sound_warning'])) {
$data['notification[sound]'] = $api['sound_warning'];
}
break;
default:
$severity = 'Unknown';
break;
}
switch ($obj['state']) {
case AlertState::RECOVERED:
$title_text = 'OK';
if (! empty($api['sound_ok'])) {
$data['notification[sound]'] = $api['sound_ok'];
}
break;
case AlertState::ACTIVE:
$title_text = $severity;
break;
case AlertState::ACKNOWLEDGED:
$title_text = 'Acknowledged';
break;
default:
$title_text = $severity;
break;
}
$data['notification[title]'] = $title_text . ' - ' . $obj['hostname'] . ' - ' . $obj['name'];
$message_text = 'Timestamp: ' . $obj['timestamp'];
if (! empty($obj['faults'])) {
$message_text .= "\n\nFaults:\n";
foreach ($obj['faults'] as $k => $faults) {
$message_text .= '#' . $k . ' ' . $faults['string'] . "\n";
}
}
$data['notification[long_message]'] = $message_text;
$curl = curl_init();
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_URL, 'https://new.boxcar.io/api/notifications');
curl_setopt($curl, CURLOPT_SAFE_UPLOAD, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 201) {
var_dump('Boxcar returned error'); //FIXME: proper debugging
return false;
}
return true;
}
public static function configTemplate()
{
return [
'config' => [
[
'title' => 'Access Token',
'name' => 'boxcar-token',
'descr' => 'Boxcar Access Token',
'type' => 'text',
],
[
'title' => 'Boxcar Options',
'name' => 'boxcar-options',
'descr' => 'Boxcar Options',
'type' => 'textarea',
],
],
'validation' => [
'boxcar-token' => 'required',
],
];
}
}

View File

@ -32,9 +32,9 @@ use Notification;
class Browserpush extends Transport class Browserpush extends Transport
{ {
protected $name = 'Browser Push'; protected string $name = 'Browser Push';
public function deliverAlert($alert_data, $opts) public function deliverAlert(array $alert_data): bool
{ {
$users = User::when($this->config['user'] ?? 0, function ($query, $user_id) { $users = User::when($this->config['user'] ?? 0, function ($query, $user_id) {
return $query->where('user_id', $user_id); return $query->where('user_id', $user_id);
@ -49,7 +49,7 @@ class Browserpush extends Transport
return true; return true;
} }
public static function configTemplate() public static function configTemplate(): array
{ {
$users = [__('All Users') => 0]; $users = [__('All Users') => 0];
foreach (User::get(['user_id', 'username', 'realname']) as $user) { foreach (User::get(['user_id', 'username', 'realname']) as $user) {

View File

@ -9,27 +9,14 @@ use PhpAmqpLib\Message\AMQPMessage;
class Canopsis extends Transport class Canopsis extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{
if (! empty($this->config)) {
$opts['host'] = $this->config['canopsis-host'];
$opts['port'] = $this->config['canopsis-port'];
$opts['user'] = $this->config['canopsis-user'];
$opts['pass'] = $this->config['canopsis-pass'];
$opts['vhost'] = $this->config['canopsis-vhost'];
}
return $this->contactCanopsis($obj, $opts);
}
public function contactCanopsis($obj, $opts)
{ {
// Configurations // Configurations
$host = $opts['host']; $host = $this->config['canopsis-host'];
$port = $opts['port']; $port = $this->config['canopsis-port'];
$user = $opts['user']; $user = $this->config['canopsis-user'];
$pass = $opts['pass']; $pass = $this->config['canopsis-pass'];
$vhost = $opts['vhost']; $vhost = $this->config['canopsis-vhost'];
$exchange = 'canopsis.events'; $exchange = 'canopsis.events';
// Connection // Connection
@ -41,7 +28,7 @@ class Canopsis extends Transport
$ch->exchange_declare($exchange, AMQPExchangeType::TOPIC, false, true, false); $ch->exchange_declare($exchange, AMQPExchangeType::TOPIC, false, true, false);
// Create Canopsis event, see: https://github.com/capensis/canopsis/wiki/Event-specification // Create Canopsis event, see: https://github.com/capensis/canopsis/wiki/Event-specification
switch ($obj['severity']) { switch ($alert_data['severity']) {
case 'ok': case 'ok':
$state = 0; $state = 0;
break; break;
@ -60,10 +47,10 @@ class Canopsis extends Transport
'connector_name' => 'LibreNMS1', 'connector_name' => 'LibreNMS1',
'event_type' => 'check', 'event_type' => 'check',
'source_type' => 'resource', 'source_type' => 'resource',
'component' => $obj['hostname'], 'component' => $alert_data['hostname'],
'resource' => $obj['name'], 'resource' => $alert_data['name'],
'state' => $state, 'state' => $state,
'output' => $obj['msg'], 'output' => $alert_data['msg'],
'display_name' => 'librenms', 'display_name' => 'librenms',
]; ];
$msg_raw = json_encode($msg_body); $msg_raw = json_encode($msg_body);
@ -84,7 +71,7 @@ class Canopsis extends Transport
return true; return true;
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -13,75 +13,41 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Ciscospark extends Transport class Ciscospark extends Transport
{ {
protected $name = 'Cisco Spark'; protected string $name = 'Cisco Webex Teams';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
if (empty($this->config)) { $room_id = $this->config['room-id'];
$room_id = $opts['roomid']; $token = $this->config['api-token'];
$token = $opts['token']; $url = 'https://api.ciscospark.com/v1/messages';
} else {
$room_id = $this->config['room-id'];
$token = $this->config['api-token'];
}
return $this->contactCiscospark($obj, $room_id, $token);
}
public function contactCiscospark($obj, $room_id, $token)
{
$text = null;
$data = [ $data = [
'roomId' => $room_id, 'roomId' => $room_id,
]; ];
$akey = 'text';
if ($this->config['use-markdown'] === 'on') { if ($this->config['use-markdown'] === 'on') {
$lines = explode("\n", $obj['msg']); // Remove blank lines as they create weird markdown behaviors.
$mlines = []; $data['markdown'] = preg_replace('/^\s+/m', '', $alert_data['msg']);
/* Remove blank lines as they create weird markdown
* behaviors.
*/
foreach ($lines as $line) {
$line = trim($line);
if ($line != '') {
array_push($mlines, $line);
}
}
$text = implode("\n", $mlines);
$akey = 'markdown';
} else { } else {
$text = strip_tags($obj['msg']); $data['text'] = strip_tags($alert_data['msg']);
}
$data[$akey] = $text;
$curl = curl_init();
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_URL, 'https://api.ciscospark.com/v1/messages');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Content-type' => 'application/json',
'Expect:',
'Authorization: Bearer ' . $token,
]);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200) {
echo "Cisco Spark returned Error, retry later\r\n";
return false;
} }
return true; $res = Http::client()
->withToken($token)
->post($url, $data);
if ($res->successful()) {
return true;
}
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $data['text'] ?? $data['markdown'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -24,39 +24,30 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Clickatell extends Transport class Clickatell extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$clickatell_opts['token'] = $this->config['clickatell-token']; $url = 'https://platform.clickatell.com/messages/http/send';
$clickatell_opts['to'] = preg_split('/([,\r\n]+)/', $this->config['clickatell-numbers']); $params = [
'apiKey' => $this->config['clickatell-token'],
'to' => implode(',', preg_split('/([,\r\n]+)/', $this->config['clickatell-numbers'])),
'content' => $alert_data['title'],
];
return $this->contactClickatell($obj, $clickatell_opts); $res = Http::client()->get($url, $params);
}
public static function contactClickatell($obj, $opts) if ($res->successful()) {
{ return true;
$url = 'https://platform.clickatell.com/messages/http/send?apiKey=' . $opts['token'] . '&to=' . implode(',', $opts['to']) . '&content=' . urlencode($obj['title']);
$curl = curl_init($url);
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code > 200) {
var_dump($ret);
return;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $alert_data['title'], $params);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -31,7 +31,7 @@ namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Exceptions\AlertTransportDeliveryException; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Proxy; use LibreNMS\Util\Http;
class Discord extends Transport class Discord extends Transport
{ {
@ -43,26 +43,16 @@ class Discord extends Transport
'rule' => 'Rule', 'rule' => 'Rule',
]; ];
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$discord_opts = [ $added_fields = $this->parseUserOptions($this->config['options']);
'url' => $this->config['url'],
'options' => $this->parseUserOptions($this->config['options']),
];
return $this->contactDiscord($obj, $discord_opts); $discord_title = '#' . $alert_data['uid'] . ' ' . $alert_data['title'];
} $discord_msg = $alert_data['msg'];
$color = hexdec(preg_replace('/[^\dA-Fa-f]/', '', self::getColorForState($alert_data['state'])));
public function contactDiscord($obj, $discord_opts)
{
$host = $discord_opts['url'];
$curl = curl_init();
$discord_title = '#' . $obj['uid'] . ' ' . $obj['title'];
$discord_msg = $obj['msg'];
$color = hexdec(preg_replace('/[^\dA-Fa-f]/', '', self::getColorForState($obj['state'])));
// Special handling for the elapsed text in the footer if the elapsed is not set. // Special handling for the elapsed text in the footer if the elapsed is not set.
$footer_text = $obj['elapsed'] ? 'alert took ' . $obj['elapsed'] : ''; $footer_text = $alert_data['elapsed'] ? 'alert took ' . $alert_data['elapsed'] : '';
$data = [ $data = [
'embeds' => [ 'embeds' => [
@ -70,36 +60,29 @@ class Discord extends Transport
'title' => $discord_title, 'title' => $discord_title,
'color' => $color, 'color' => $color,
'description' => $discord_msg, 'description' => $discord_msg,
'fields' => $this->createDiscordFields($obj, $discord_opts), 'fields' => $this->createDiscordFields($alert_data),
'footer' => [ 'footer' => [
'text' => $footer_text, 'text' => $footer_text,
], ],
], ],
], ],
]; ];
if (! empty($discord_opts['options'])) { if (! empty($added_fields)) {
$data = array_merge($data, $discord_opts['options']); $data = array_merge($data, $added_fields);
} }
$data = $this->embedGraphs($data); $data = $this->embedGraphs($data);
// remove all remaining HTML tags // remove all remaining HTML tags
$data['embeds'][0]['description'] = strip_tags($data['embeds'][0]['description']); $data['embeds'][0]['description'] = strip_tags($data['embeds'][0]['description']);
$alert_message = json_encode($data);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_URL, $host);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $alert_message);
$ret = curl_exec($curl); $res = Http::client()->post($this->config['url'], $data);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 204) { if ($res->successful()) {
throw new AlertTransportDeliveryException($obj, $code, $ret, $alert_message, $data); return true;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $discord_msg, $data);
} }
private function embedGraphs(array $data): array private function embedGraphs(array $data): array
@ -118,26 +101,26 @@ class Discord extends Transport
return $data; return $data;
} }
public function createDiscordFields($obj, $discord_opts) public function createDiscordFields(array $alert_data): array
{ {
$result = []; $result = [];
foreach (self::ALERT_FIELDS_TO_DISCORD_FIELDS as $objKey => $discordKey) { foreach (self::ALERT_FIELDS_TO_DISCORD_FIELDS as $objKey => $discordKey) {
// Skip over keys that do not exist so Discord does not give us a 400. // Skip over keys that do not exist so Discord does not give us a 400.
if (! $obj[$objKey]) { if (! $alert_data[$objKey]) {
continue; continue;
} }
array_push($result, [ $result[] = [
'name' => $discordKey, 'name' => $discordKey,
'value' => $obj[$objKey], 'value' => $alert_data[$objKey],
]); ];
} }
return $result; return $result;
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -24,22 +24,16 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Exceptions\AlertTransportDeliveryException;
class Dummy extends Transport class Dummy extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
var_dump($obj); throw new AlertTransportDeliveryException($alert_data, 0, 'Dummy transport always fails', $alert_data['msg']);
return true;
} }
public function contactDummy() public static function configTemplate(): array
{
return true;
}
public static function configTemplate()
{ {
return [ return [
'validation' => [], 'validation' => [],

View File

@ -19,191 +19,130 @@ namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Elasticsearch extends Transport class Elasticsearch extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
if (! empty($this->config)) { $es_host = $this->config['es-host'];
$opts['es_host'] = $this->config['es-host']; $es_port = $this->config['es-port'] ?: 9200;
$opts['es_port'] = $this->config['es-port']; $index = date($this->config['es-pattern'] ?: "\l\i\b\\r\\e\\n\m\s\-Y.m.d");
$opts['es_index'] = $this->config['es-pattern'];
$opts['es_proxy'] = $this->config['es-proxy'];
}
return $this->contactElasticsearch($obj, $opts);
}
public function contactElasticsearch($obj, $opts)
{
$es_host = '127.0.0.1';
$es_port = 9200;
$index = date("\l\i\b\\r\\e\\n\m\s\-Y.m.d");
$type = 'alert'; $type = 'alert';
$severity = $obj['severity']; $severity = $alert_data['severity'];
if (! empty($opts['es_host'])) {
if (preg_match('/[a-zA-Z]/', $opts['es_host'])) {
$es_host = gethostbyname($opts['es_host']);
if ($es_host === $opts['es_host']) {
return 'Alphanumeric hostname found but does not resolve to an IP.';
}
} elseif (filter_var($opts['es_host'], FILTER_VALIDATE_IP)) {
$es_host = $opts['es_host'];
} else {
return 'Elasticsearch host is not a valid IP: ' . $opts['es_host'];
}
}
if (! empty($opts['es_port']) && preg_match("/^\d+$/", $opts['es_port'])) {
$es_port = $opts['es_port'];
}
if (! empty($opts['es_index'])) {
$index = date($opts['es_index']);
}
$host = $es_host . ':' . $es_port . '/' . $index . '/' . $type; $host = $es_host . ':' . $es_port . '/' . $index . '/' . $type;
switch ($obj['state']) { $state = match ($alert_data['state']) {
case AlertState::RECOVERED: AlertState::RECOVERED => 'ok',
$state = 'ok'; AlertState::ACTIVE => $severity,
break; AlertState::ACKNOWLEDGED => 'acknowledged',
case AlertState::ACTIVE: AlertState::WORSE => 'worse',
$state = $severity; AlertState::BETTER => 'better',
break; default => 'unknown',
case AlertState::ACKNOWLEDGED: };
$state = 'acknowledged';
break;
case AlertState::WORSE:
$state = 'worse';
break;
case AlertState::BETTER:
$state = 'better';
break;
default:
$state = 'unknown';
break;
}
$data = [ $data = [
'@timestamp' => date('c'), '@timestamp' => date('c'),
'host' => gethostname(), 'host' => gethostname(),
'location' => $obj['location'], 'location' => $alert_data['location'],
'title' => $obj['name'], 'title' => $alert_data['name'],
'message' => $obj['string'], 'message' => $alert_data['string'],
'device_id' => $obj['device_id'], 'device_id' => $alert_data['device_id'],
'device_name' => $obj['hostname'], 'device_name' => $alert_data['hostname'],
'device_hardware' => $obj['hardware'], 'device_hardware' => $alert_data['hardware'],
'device_version' => $obj['version'], 'device_version' => $alert_data['version'],
'state' => $state, 'state' => $state,
'severity' => $severity, 'severity' => $severity,
'first_occurrence' => $obj['timestamp'], 'first_occurrence' => $alert_data['timestamp'],
'entity_type' => 'device', 'entity_type' => 'device',
'entity_tab' => 'overview', 'entity_tab' => 'overview',
'entity_id' => $obj['device_id'], 'entity_id' => $alert_data['device_id'],
'entity_name' => $obj['hostname'], 'entity_name' => $alert_data['hostname'],
'entity_descr' => $obj['sysDescr'], 'entity_descr' => $alert_data['sysDescr'],
]; ];
if (! empty($obj['faults'])) { foreach ($alert_data['faults'] as $k => $v) {
foreach ($obj['faults'] as $k => $v) { $data['message'] = $v['string'];
$curl = curl_init(); switch (true) {
$data['message'] = $v['string']; case array_key_exists('port_id', $v):
switch (true) { $data['entity_type'] = 'port';
case array_key_exists('port_id', $v): $data['entity_tab'] = 'port';
$data['entity_type'] = 'port'; $data['entity_id'] = $v['port_id'];
$data['entity_tab'] = 'port'; $data['entity_name'] = $v['ifName'];
$data['entity_id'] = $v['port_id']; $data['entity_descr'] = $v['ifAlias'];
$data['entity_name'] = $v['ifName']; break;
$data['entity_descr'] = $v['ifAlias']; case array_key_exists('sensor_id', $v):
break; $data['entity_type'] = $v['sensor_class'];
case array_key_exists('sensor_id', $v): $data['entity_tab'] = 'health';
$data['entity_type'] = $v['sensor_class']; $data['entity_id'] = $v['sensor_id'];
$data['entity_tab'] = 'health'; $data['entity_name'] = $v['sensor_descr'];
$data['entity_id'] = $v['sensor_id']; $data['entity_descr'] = $v['sensor_type'];
$data['entity_name'] = $v['sensor_descr']; break;
$data['entity_descr'] = $v['sensor_type']; case array_key_exists('mempool_id', $v):
break; $data['entity_type'] = 'mempool';
case array_key_exists('mempool_id', $v): $data['entity_tab'] = 'health';
$data['entity_type'] = 'mempool'; $data['entity_id'] = $v['mempool_id'];
$data['entity_tab'] = 'health'; $data['entity_name'] = $v['mempool_index'];
$data['entity_id'] = $v['mempool_id']; $data['entity_descr'] = $v['mempool_descr'];
$data['entity_name'] = $v['mempool_index']; break;
$data['entity_descr'] = $v['mempool_descr']; case array_key_exists('storage_id', $v):
break; $data['entity_type'] = 'storage';
case array_key_exists('storage_id', $v): $data['entity_tab'] = 'health';
$data['entity_type'] = 'storage'; $data['entity_id'] = $v['storage_id'];
$data['entity_tab'] = 'health'; $data['entity_name'] = $v['storage_index'];
$data['entity_id'] = $v['storage_id']; $data['entity_descr'] = $v['storage_descr'];
$data['entity_name'] = $v['storage_index']; break;
$data['entity_descr'] = $v['storage_descr']; case array_key_exists('processor_id', $v):
break; $data['entity_type'] = 'processor';
case array_key_exists('processor_id', $v): $data['entity_tab'] = 'health';
$data['entity_type'] = 'processor'; $data['entity_id'] = $v['processor_id'];
$data['entity_tab'] = 'health'; $data['entity_name'] = $v['processor_type'];
$data['entity_id'] = $v['processor_id']; $data['entity_descr'] = $v['processor_descr'];
$data['entity_name'] = $v['processor_type']; break;
$data['entity_descr'] = $v['processor_descr']; case array_key_exists('bgpPeer_id', $v):
break; $data['entity_type'] = 'bgp';
case array_key_exists('bgpPeer_id', $v): $data['entity_tab'] = 'routing';
$data['entity_type'] = 'bgp'; $data['entity_id'] = $v['bgpPeer_id'];
$data['entity_tab'] = 'routing'; $data['entity_name'] = 'local: ' . $v['bgpPeerLocalAddr'] . ' - AS' . $alert_data['bgpLocalAs'];
$data['entity_id'] = $v['bgpPeer_id']; $data['entity_descr'] = 'remote: ' . $v['bgpPeerIdentifier'] . ' - AS' . $v['bgpPeerRemoteAs'];
$data['entity_name'] = 'local: ' . $v['bgpPeerLocalAddr'] . ' - AS' . $obj['bgpLocalAs']; break;
$data['entity_descr'] = 'remote: ' . $v['bgpPeerIdentifier'] . ' - AS' . $v['bgpPeerRemoteAs']; case array_key_exists('tunnel_id', $v):
break; $data['entity_type'] = 'ipsec_tunnel';
case array_key_exists('tunnel_id', $v): $data['entity_tab'] = 'routing';
$data['entity_type'] = 'ipsec_tunnel'; $data['entity_id'] = $v['tunnel_id'];
$data['entity_tab'] = 'routing'; $data['entity_name'] = $v['tunnel_name'];
$data['entity_id'] = $v['tunnel_id']; $data['entity_descr'] = 'local: ' . $v['local_addr'] . ':' . $v['local_port'] . ', remote: ' . $v['peer_addr'] . ':' . $v['peer_port'];
$data['entity_name'] = $v['tunnel_name']; break;
$data['entity_descr'] = 'local: ' . $v['local_addr'] . ':' . $v['local_port'] . ', remote: ' . $v['peer_addr'] . ':' . $v['peer_port']; default:
break; $data['entity_type'] = 'generic';
default: break;
$data['entity_type'] = 'generic';
break;
}
$alert_message = json_encode($data);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
if ($opts['es_proxy'] === true) {
Proxy::applyToCurl($curl);
}
curl_setopt($curl, CURLOPT_URL, $host);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $alert_message);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200 && $code != 201) {
return $host . ' returned HTTP Status code ' . $code . ' for ' . $alert_message;
}
}
} else {
$curl = curl_init();
$alert_message = json_encode($data);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
if ($opts['es_proxy'] === true) {
Proxy::applyToCurl($curl);
}
curl_setopt($curl, CURLOPT_URL, $host);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $alert_message);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200 && $code != 201) {
return $host . ' returned HTTP Status code ' . $code . ' for ' . $alert_message;
} }
} }
return true; $client = Http::client();
// silly, just use no_proxy
if ($this->config['es-proxy'] !== 'on') {
$client->withOptions([
'proxy' => [
'http' => '',
'https' => '',
],
]);
}
$res = $client->post($host, $data);
if ($res->successful()) {
return true;
}
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $data['message'] ?? '', $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [
@ -212,31 +151,34 @@ class Elasticsearch extends Transport
'name' => 'es-host', 'name' => 'es-host',
'descr' => 'Elasticsearch Host', 'descr' => 'Elasticsearch Host',
'type' => 'text', 'type' => 'text',
'default' => '127.0.0.1',
], ],
[ [
'title' => 'Port', 'title' => 'Port',
'name' => 'es-port', 'name' => 'es-port',
'descr' => 'Elasticsearch Port', 'descr' => 'Elasticsearch Port',
'type' => 'text', 'type' => 'text',
'default' => 9200,
], ],
[ [
'title' => 'Index Pattern', 'title' => 'Index Pattern',
'name' => 'es-pattern', 'name' => 'es-pattern',
'descr' => 'Elasticsearch Index Pattern | Default: \l\i\b\\r\\e\\n\m\s\-Y.m.d | Format: https://www.php.net/manual/en/function.date.php', 'descr' => 'Elasticsearch Index Pattern | Default: \l\i\b\\r\\e\\n\m\s\-Y.m.d | Format: https://www.php.net/manual/en/function.date.php',
'type' => 'text', 'type' => 'text',
'default' => "\l\i\b\\r\\e\\n\m\s\-Y.m.d",
], ],
[ [
'title' => 'Use proxy if configured?', 'title' => 'Use proxy if configured?',
'name' => 'es-proxy', 'name' => 'es-proxy',
'descr' => 'Elasticsearch Proxy', 'descr' => 'Elasticsearch Proxy (Deprecated: just use no_proxy setting to exclude ES server)',
'type' => 'checkbox', 'type' => 'checkbox',
'default' => false, 'default' => true,
], ],
], ],
'validation' => [ 'validation' => [
'es-host' => 'required|string', 'es-host' => 'required|ip_or_hostname',
'es-port' => 'required|string', 'es-port' => 'integer|between:1,65535',
'es-pattern' => 'required|string', 'es-pattern' => 'string',
], ],
]; ];
} }

View File

@ -25,67 +25,40 @@ namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Gitlab extends Transport class Gitlab extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{
if (! empty($this->config)) {
$opts['project-id'] = $this->config['gitlab-id'];
$opts['key'] = $this->config['gitlab-key'];
$opts['host'] = $this->config['gitlab-host'];
}
return $this->contactGitlab($obj, $opts);
}
public function contactGitlab($obj, $opts)
{ {
// Don't create tickets for resolutions // Don't create tickets for resolutions
if ($obj['state'] != AlertState::CLEAR) { if ($alert_data['state'] == AlertState::RECOVERED) {
$project_id = $opts['project-id']; return true;
$project_key = $opts['key'];
$details = 'Librenms alert for: ' . $obj['hostname'];
$description = $obj['msg'];
$title = urlencode($details);
$desc = urlencode($description);
$url = $opts['host'] . "/api/v4/projects/$project_id/issues?title=$title&description=$desc";
$curl = curl_init();
$data = ['title' => $details,
'description' => $description,
];
$postdata = ['fields' => $data];
$datastring = json_encode($postdata);
Proxy::applyToCurl($curl);
$headers = ['Accept: application/json', 'Content-Type: application/json', 'PRIVATE-TOKEN: ' . $project_key];
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_VERBOSE, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $datastring);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code == 200) {
$gitlabout = json_decode($ret, true);
d_echo('Created GitLab issue ' . $gitlabout['key'] . ' for ' . $obj['hostname']);
return true;
} else {
d_echo('GitLab connection error: ' . serialize($ret));
return false;
}
} }
$project_id = $this->config['gitlab-id'];
$url = $this->config['gitlab-host'] . "/api/v4/projects/$project_id/issues";
$data = [
'title' => 'Librenms alert for: ' . $alert_data['hostname'],
'description' => $alert_data['msg'],
];
$res = Http::client()
->withHeaders([
'PRIVATE-TOKEN' => $this->config['gitlab-key'],
])
->acceptJson()
->post($url, $data);
if ($res->successful()) {
return true;
}
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $data['description'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -24,60 +24,26 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use Log; use LibreNMS\Util\Http;
class Googlechat extends Transport class Googlechat extends Transport
{ {
protected $name = 'Google Chat'; protected string $name = 'Google Chat';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$googlechat_conf['webhookurl'] = $this->config['googlechat-webhook']; $data = ['text' => $alert_data['msg']];
$res = Http::client()->post($this->config['googlechat-webhook'], $data);
return $this->contactGooglechat($obj, $googlechat_conf); if ($res->successful()) {
} return true;
public static function contactGooglechat($obj, $data)
{
$payload = '{"text": "' . $obj['msg'] . '"}';
Log::debug($payload);
// Create a new cURL resource
$ch = curl_init($data['webhookurl']);
Proxy::applyToCurl($ch);
// Attach encoded JSON string to the POST fields
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
// Set the content type to application/json
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type:application/json']);
// Return response instead of outputting
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Execute the POST request
$result = curl_exec($ch);
// Close cURL resource
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
Log::debug("$code");
if ($code != 200) {
Log::error('Google Chat Transport Error');
Log::error($result);
return 'HTTP Status code ' . $code;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $data['text'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -25,92 +25,73 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Enum\AlertState;
use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Hipchat extends Transport class Hipchat extends Transport
{ {
protected $name = 'HipChat'; protected string $name = 'HipChat';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$hipchat_opts = $this->parseUserOptions($this->config['hipchat-options']); $options = $this->parseUserOptions($this->config['hipchat-options']);
$hipchat_opts['url'] = $this->config['hipchat-url'];
$hipchat_opts['room_id'] = $this->config['hipchat-room-id'];
$hipchat_opts['from'] = $this->config['hipchat-from-name'];
return $this->contactHipchat($obj, $hipchat_opts); // override legacy options
} if (array_key_exists('hipchat-notify', $this->config)) {
$options['notify'] = ($this->config['hipchat-notify'] == 'on');
public function contactHipchat($obj, $option)
{
$version = 1;
if (stripos($option['url'], 'v2')) {
$version = 2;
} }
if (isset($this->config['hipchat-message_format'])) {
$options['message_format'] = $this->config['hipchat-message_format'];
}
$url = $this->config['hipchat-url'];
$version = str_contains($url, 'v2') ? 2 : 1;
// Generate our URL from the base URL + room_id and the auth token if the version is 2. // Generate our URL from the base URL + room_id and the auth token if the version is 2.
$url = $option['url'];
if ($version == 2) { if ($version == 2) {
$url .= '/' . urlencode($option['room_id']) . '/notification?auth_token=' . urlencode($option['auth_token']); $url .= '/' . urlencode($this->config['hipchat-room-id']) . '/notification';
}
$curl = curl_init();
if (empty($obj['msg'])) {
return 'Empty Message';
}
if (empty($option['message_format'])) {
$option['message_format'] = 'text';
} }
// Sane default of making the message color green if the message indicates // Sane default of making the message color green if the message indicates
// that the alert recovered. If it rebooted, make it yellow. // that the alert recovered. If it rebooted, make it yellow.
if (stripos($obj['msg'], 'recovered')) { if ($alert_data['state'] == AlertState::RECOVERED) {
$color = 'green'; $color = 'green';
} elseif (stripos($obj['msg'], 'rebooted')) { } elseif (str_contains($alert_data['msg'], 'rebooted')) {
$color = 'yellow'; $color = 'yellow';
} elseif (empty($options['color']) || $options['color'] == 'u') {
$color = 'red';
} else { } else {
if (empty($option['color']) || $option['color'] == 'u') { $color = $options['color'];
$color = 'red';
} else {
$color = $option['color'];
}
} }
$data[] = 'message=' . urlencode($obj['msg']); $data = [
'message' => $alert_data['msg'],
'from' => $this->config['hipchat-from-name'] ?: 'LibreNMS',
'color' => $color,
'notify' => $options['notify'] ?? '1',
'message_format' => $options['message_format'] ?: 'text',
];
if ($version == 1) { if ($version == 1) {
$data[] = 'room_id=' . urlencode($option['room_id']); $data['room_id'] = $this->config['hipchat-room-id'];
}
$data[] = 'from=' . urlencode($option['from']);
$data[] = 'color=' . urlencode($color);
$data[] = 'notify=' . urlencode($option['notify']);
$data[] = 'message_format=' . urlencode($option['message_format']);
$data = implode('&', $data);
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded',
]);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200 && $code != 204) {
var_dump("API '$url' returned Error");
//var_dump('Params: ' . $message);
var_dump('Return: ' . $ret);
return 'HTTP Status code ' . $code;
} }
return true; $client = Http::client();
if ($version == 2) {
$client->withToken($options['auth_token']);
}
$res = $client->post($url, $data);
if ($res->successful()) {
return true;
}
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $data['message'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [
@ -138,6 +119,24 @@ class Hipchat extends Transport
'descr' => 'Hipchat Options', 'descr' => 'Hipchat Options',
'type' => 'textarea', 'type' => 'textarea',
], ],
[
'title' => 'Notify?',
'name' => 'hipchat-notify',
'descr' => 'Send notification',
'type' => 'checkbox',
'default' => 'on',
],
[
'title' => 'Message Format',
'name' => 'hipchat-message_format',
'descr' => 'Format of message',
'type' => 'select',
'options' => [
'Text' => 'text',
'HTML' => 'html',
],
'default' => 'text',
],
], ],
'validation' => [ 'validation' => [
'hipchat-url' => 'required|url', 'hipchat-url' => 'required|url',

View File

@ -26,7 +26,8 @@ namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
/** /**
* The Hue API currently is fairly limited for alerts. * The Hue API currently is fairly limited for alerts.
@ -35,56 +36,30 @@ use LibreNMS\Util\Proxy;
*/ */
class Hue extends Transport class Hue extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{
if (! empty($this->config)) {
$opts['user'] = $this->config['hue-user'];
$opts['bridge'] = $this->config['hue-host'];
$opts['duration'] = $this->config['hue-duration'];
}
return $this->contactHue($obj, $opts);
}
public function contactHue($obj, $opts)
{ {
// Don't alert on resolve at this time // Don't alert on resolve at this time
if ($obj['state'] == AlertState::RECOVERED) { if ($alert_data['state'] == AlertState::RECOVERED) {
return true; return true;
} else {
$hue_user = $opts['user'];
$url = $opts['bridge'] . "/api/$hue_user/groups/0/action";
$curl = curl_init();
$duration = $opts['duration'];
$data = ['alert' => $duration];
$datastring = json_encode($data);
Proxy::applyToCurl($curl);
$headers = ['Accept: application/json', 'Content-Type: application/json'];
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_VERBOSE, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $datastring);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code == 200) {
d_echo('Sent alert to Phillips Hue Bridge ' . $opts['host'] . ' for ' . $obj['hostname']);
return true;
} else {
d_echo('Hue bridge connection error: ' . serialize($ret));
return false;
}
} }
$hue_user = $this->config['hue-user'];
$url = $this->config['hue-host'] . "/api/$hue_user/groups/0/action";
$duration = $this->config['hue-duration'];
$data = ['alert' => $duration];
$res = Http::client()
->acceptJson()
->put($url, $data);
if ($res->successful()) {
return true;
}
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $duration, $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config'=>[ 'config'=>[

View File

@ -25,17 +25,13 @@ namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Config; use LibreNMS\Config;
use LibreNMS\Exceptions\AlertTransportDeliveryException;
class Irc extends Transport class Irc extends Transport
{ {
protected $name = 'IRC'; protected string $name = 'IRC';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{
return $this->contactIrc($obj, $opts);
}
public function contactIrc($obj, $opts)
{ {
$container_dir = '/data'; $container_dir = '/data';
if (file_exists($container_dir) and posix_getpwuid(fileowner($container_dir))['name'] == 'librenms') { if (file_exists($container_dir) and posix_getpwuid(fileowner($container_dir))['name'] == 'librenms') {
@ -45,19 +41,20 @@ class Irc extends Transport
} }
if (file_exists($f) && filetype($f) == 'fifo') { if (file_exists($f) && filetype($f) == 'fifo') {
$f = fopen($f, 'w+'); $f = fopen($f, 'w+');
$r = fwrite($f, json_encode($obj) . "\n"); $r = fwrite($f, json_encode($alert_data) . "\n");
$f = fclose($f); fclose($f);
if ($r === false) { if ($r === false) {
return false; throw new AlertTransportDeliveryException($alert_data, 0, 'Could not write to fifo', $alert_data['msg'], $alert_data);
} else {
return true;
} }
return true;
} }
return false; throw new AlertTransportDeliveryException($alert_data, 0, 'fifo does not exist', $alert_data['msg'], $alert_data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -24,73 +24,50 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Jira extends Transport class Jira extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{
if (! empty($this->config)) {
$opts['username'] = $this->config['jira-username'];
$opts['password'] = $this->config['jira-password'];
$opts['prjkey'] = $this->config['jira-key'];
$opts['issuetype'] = $this->config['jira-type'];
$opts['url'] = $this->config['jira-url'];
}
return $this->contactJira($obj, $opts);
}
public function contactJira($obj, $opts)
{ {
// Don't create tickets for resolutions // Don't create tickets for resolutions
if ($obj['severity'] == 'recovery' && $obj['msg'] != 'This is a test alert') { if ($alert_data['severity'] == 'recovery') {
return true; return true;
} }
$username = $opts['username']; $prjkey = $this->config['jira-key'];
$password = $opts['password']; $issuetype = $this->config['jira-type'];
$prjkey = $opts['prjkey']; $details = empty($alert_data['title']) ? 'Librenms alert for: ' . $alert_data['hostname'] : $alert_data['title'];
$issuetype = $opts['issuetype']; $description = $alert_data['msg'];
$details = empty($obj['title']) ? 'Librenms alert for: ' . $obj['hostname'] : $obj['title']; $url = $this->config['jira-url'] . '/rest/api/latest/issue';
$description = $obj['msg'];
$url = $opts['url'] . '/rest/api/latest/issue';
$curl = curl_init();
$data = ['project' => ['key' => $prjkey], $data = [
'summary' => $details, 'fields' => [
'description' => $description, 'project' => [
'issuetype' => ['name' => $issuetype], ]; 'key' => $prjkey,
$postdata = ['fields' => $data]; ],
$datastring = json_encode($postdata); 'summary' => $details,
'description' => $description,
'issuetype' => [
'name' => $issuetype,
],
],
];
Proxy::applyToCurl($curl); $res = Http::client()
->withBasicAuth($this->config['jira-username'], $this->config['jira-password'])
$headers = ['Accept: application/json', 'Content-Type: application/json']; ->acceptJson()
->post($url, $data);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_USERPWD, "$username:$password");
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_VERBOSE, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $datastring);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code == 200 || $code == 201) {
$jiraout = json_decode($ret, true);
d_echo('Created jira issue ' . $jiraout['key'] . ' for ' . $obj['hostname']);
if ($res->successful()) {
return true; return true;
} else {
d_echo('Jira connection error: ' . serialize($ret));
return false;
} }
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $description, $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -14,29 +14,18 @@ namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Config; use LibreNMS\Config;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Kayako extends Transport class Kayako extends Transport
{ {
public function deliverAlert($host, $kayako) public function deliverAlert(array $alert_data): bool
{ {
if (! empty($this->config)) { $url = $this->config['kayako-url'] . '/Tickets/Ticket';
$kayako['url'] = $this->config['kayako-url']; $key = $this->config['kayako-key'];
$kayako['key'] = $this->config['kayako-key']; $secret = $this->config['kayako-secret'];
$kayako['secret'] = $this->config['kayako-secret'];
$kayako['department'] = $this->config['kayako-department'];
}
return $this->contactKayako($host, $kayako);
}
public function contactKayako($host, $kayako)
{
$url = $kayako['url'] . '/Tickets/Ticket';
$key = $kayako['key'];
$secret = $kayako['secret'];
$user = Config::get('email_from'); $user = Config::get('email_from');
$department = $kayako['department']; $department = $this->config['kayako-department'];
$ticket_type = 1; $ticket_type = 1;
$ticket_status = 1; $ticket_status = 1;
$ticket_prio = 1; $ticket_prio = 1;
@ -44,10 +33,10 @@ class Kayako extends Transport
$signature = base64_encode(hash_hmac('sha256', $salt, $secret, true)); $signature = base64_encode(hash_hmac('sha256', $salt, $secret, true));
$protocol = [ $protocol = [
'subject' => ($host['name'] ? $host['name'] . ' on ' . $host['hostname'] : $host['title']), 'subject' => ($alert_data['name'] ? $alert_data['name'] . ' on ' . $alert_data['hostname'] : $alert_data['title']),
'fullname' => 'LibreNMS Alert', 'fullname' => 'LibreNMS Alert',
'email' => $user, 'email' => $user,
'contents' => strip_tags($host['msg']), 'contents' => strip_tags($alert_data['msg']),
'departmentid' => $department, 'departmentid' => $department,
'ticketstatusid' => $ticket_status, 'ticketstatusid' => $ticket_status,
'ticketpriorityid' => $ticket_prio, 'ticketpriorityid' => $ticket_prio,
@ -58,27 +47,19 @@ class Kayako extends Transport
'salt' => $salt, 'salt' => $salt,
'signature' => $signature, 'signature' => $signature,
]; ];
$post_data = http_build_query($protocol, '', '&');
$curl = curl_init(); $res = Http::client()
Proxy::applyToCurl($curl); ->asForm() // unsure if this is needed, can't access docs
curl_setopt($curl, CURLOPT_POST, true); ->post($url, $protocol);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $post_data);
curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE); if ($res->successful()) {
if ($code != 200) { return true;
var_dump('Kayako returned Error, retry later');
return false;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $protocol['contents'], $protocol);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -6,44 +6,32 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Linenotify extends Transport class Linenotify extends Transport
{ {
protected $name = 'LINE Notify'; protected string $name = 'LINE Notify';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{
$opts['line-notify-access-token'] = $this->config['line-notify-access-token'];
return $this->contactLinenotify($obj, $opts);
}
private function contactLinenotify($obj, $opts)
{ {
// TODO possible to attach graph images
$lineUrl = 'https://notify-api.line.me/api/notify'; $lineUrl = 'https://notify-api.line.me/api/notify';
$lineHead = ['Authorization: Bearer ' . $opts['line-notify-access-token']]; $lineFields = ['message' => $alert_data['msg']];
$lineFields = ['message' => $obj['msg']];
$curl = curl_init(); $res = Http::client()
Proxy::applyToCurl($curl); ->withToken($this->config['line-notify-access-token'])
curl_setopt($curl, CURLOPT_URL, $lineUrl); ->asForm()
curl_setopt($curl, CURLOPT_HTTPHEADER, $lineHead); ->post($lineUrl, $lineFields);
curl_setopt($curl, CURLOPT_NOBODY, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); if ($res->successful()) {
curl_setopt($curl, CURLOPT_POST, true); return true;
curl_setopt($curl, CURLOPT_POSTFIELDS, $lineFields);
curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($code != 200) {
return 'HTTP Status code ' . $code;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $alert_data['msg'], $lineFields);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -28,28 +28,23 @@ use LibreNMS\Config;
class Mail extends Transport class Mail extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
return $this->contactMail($obj); $email = $this->config['email'] ?? $alert_data['contacts'];
}
public function contactMail($obj)
{
$email = $this->config['email'] ?? $obj['contacts'];
$html = Config::get('email_html'); $html = Config::get('email_html');
if ($html && ! $this->isHtmlContent($obj['msg'])) { if ($html && ! $this->isHtmlContent($alert_data['msg'])) {
// if there are no html tags in the content, but we are sending an html email, use br for line returns instead // if there are no html tags in the content, but we are sending an html email, use br for line returns instead
$msg = preg_replace("/\r?\n/", "<br />\n", $obj['msg']); $msg = preg_replace("/\r?\n/", "<br />\n", $alert_data['msg']);
} else { } else {
// fix line returns for windows mail clients // fix line returns for windows mail clients
$msg = preg_replace("/(?<!\r)\n/", "\r\n", $obj['msg']); $msg = preg_replace("/(?<!\r)\n/", "\r\n", $alert_data['msg']);
} }
return \LibreNMS\Util\Mail::send($email, $obj['title'], $msg, $html, $this->config['attach-graph'] ?? null); return \LibreNMS\Util\Mail::send($email, $alert_data['title'], $msg, $html, $this->config['attach-graph'] ?? null);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -25,58 +25,40 @@ namespace LibreNMS\Alert\Transport;
use App\View\SimpleTemplate; use App\View\SimpleTemplate;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Matrix extends Transport class Matrix extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$server = $this->config['matrix-server']; $server = $this->config['matrix-server'];
$room = $this->config['matrix-room']; $room = $this->config['matrix-room'];
$authtoken = $this->config['matrix-authtoken']; $authtoken = $this->config['matrix-authtoken'];
$message = $this->config['matrix-message']; $message = $this->config['matrix-message'];
return $this->contactMatrix($obj, $server, $room, $authtoken, $message);
}
private function contactMatrix($obj, $server, $room, $authtoken, $message)
{
$request_opts = [];
$request_heads = [];
$txnid = rand(1111, 9999) . time(); $txnid = rand(1111, 9999) . time();
$server = preg_replace('/\/$/', '', $server); $server = preg_replace('/\/$/', '', $server);
$host = $server . '/_matrix/client/r0/rooms/' . urlencode($room) . '/send/m.room.message/' . $txnid; $host = $server . '/_matrix/client/r0/rooms/' . urlencode($room) . '/send/m.room.message/' . $txnid;
$request_heads['Authorization'] = "Bearer $authtoken"; $message = SimpleTemplate::parse($message, $alert_data);
$request_heads['Content-Type'] = 'application/json';
$request_heads['Accept'] = 'application/json';
$message = SimpleTemplate::parse($message, $obj); $body = ['body' => strip_tags($message), 'formatted_body' => "$message", 'msgtype' => 'm.text', 'format' => 'org.matrix.custom.html'];
$body = ['body'=>strip_tags($message), 'formatted_body'=>"$message", 'msgtype'=>'m.text', 'format'=>'org.matrix.custom.html']; $res = Http::client()
->withToken($authtoken)
->acceptJson()
->put($host, $body);
$client = new \GuzzleHttp\Client(); if ($res->successful()) {
$request_opts['proxy'] = Proxy::forGuzzle(); return true;
$request_opts['headers'] = $request_heads;
$request_opts['body'] = json_encode($body);
$res = $client->request('PUT', $host, $request_opts);
$code = $res->getStatusCode();
if ($code != 200) {
var_dump("Matrix '$host' returned Error");
var_dump('Params:');
var_dump('Response headers:');
var_dump($res->getHeaders());
var_dump('Return: ' . $res->getReasonPhrase());
return 'HTTP Status code ' . $code;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $message, $body);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -24,70 +24,42 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Mattermost extends Transport class Mattermost extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$mattermost_opts = [ $host = $this->config['mattermost-url'];
'url' => $this->config['mattermost-url'], $mattermost_msg = strip_tags($alert_data['msg']);
'username' => $this->config['mattermost-username'], $color = self::getColorForState($alert_data['state']);
'icon' => $this->config['mattermost-icon'],
'channel' => $this->config['mattermost-channel'],
'author_name' => $this->config['mattermost-author_name'],
];
return $this->contactMattermost($obj, $mattermost_opts);
}
public static function contactMattermost($obj, $api)
{
$host = $api['url'];
$curl = curl_init();
$mattermost_msg = strip_tags($obj['msg']);
$color = self::getColorForState($obj['state']);
$data = [ $data = [
'attachments' => [ 'attachments' => [
0 => [ 0 => [
'fallback' => $mattermost_msg, 'fallback' => $mattermost_msg,
'color' => $color, 'color' => $color,
'title' => $obj['title'], 'title' => $alert_data['title'],
'text' => $obj['msg'], 'text' => $alert_data['msg'],
'mrkdwn_in' => ['text', 'fallback'], 'mrkdwn_in' => ['text', 'fallback'],
'author_name' => $api['author_name'], 'author_name' => $this->config['mattermost-author_name'],
], ],
], ],
'channel' => $api['channel'], 'channel' => $this->config['mattermost-channel'],
'username' => $api['username'], 'username' => $this->config['mattermost-username'],
'icon_url' => $api['icon'], 'icon_url' => $this->config['mattermost-icon'],
]; ];
Proxy::applyToCurl($curl); $res = Http::client()->acceptJson()->post($host, $data);
$httpheaders = ['Accept: application/json', 'Content-Type: application/json'];
$alert_payload = json_encode($data);
curl_setopt($curl, CURLOPT_HTTPHEADER, $httpheaders);
curl_setopt($curl, CURLOPT_URL, $host);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $alert_payload);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200) {
d_echo('Mattermost Connection Error: ' . $ret);
return 'HTTP Status code ' . $code;
} else {
d_echo('Mattermost message sent for ' . $obj['hostname']);
if ($res->successful()) {
return true; return true;
} }
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $alert_data['msg'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -13,51 +13,43 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Msteams extends Transport class Msteams extends Transport
{ {
protected $name = 'Microsoft Teams'; protected string $name = 'Microsoft Teams';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
if (! empty($this->config)) {
$opts['url'] = $this->config['msteam-url'];
}
return $this->contactMsteams($obj, $opts);
}
public function contactMsteams($obj, $opts)
{
$url = $opts['url'];
$data = [ $data = [
'title' => $obj['title'], 'title' => $alert_data['title'],
'themeColor' => self::getColorForState($obj['state']), 'themeColor' => self::getColorForState($alert_data['state']),
'text' => strip_tags($obj['msg'], '<strong><em><h1><h2><h3><strike><ul><ol><li><pre><blockquote><a><img><p>'), 'text' => strip_tags($alert_data['msg'], '<strong><em><h1><h2><h3><strike><ul><ol><li><pre><blockquote><a><img><p>'),
'summary' => $obj['title'], 'summary' => $alert_data['title'],
]; ];
$curl = curl_init();
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type:application/json', 'Expect:']);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
if ($this->config['use-json'] === 'on' && $obj['uid'] !== '000') {
curl_setopt($curl, CURLOPT_POSTFIELDS, $obj['msg']);
}
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200) {
var_dump('Microsoft Teams returned Error, retry later');
return false; $client = Http::client();
// template will contain raw json
if ($this->config['use-json'] === 'on') {
$msg = $alert_data['uid'] === '000'
? $this->messageCard() // use pre-made MessageCard for tests
: $alert_data['msg'];
$client->withBody($msg, 'application/json');
} }
return true; $res = $client->post($this->config['msteam-url'], $data);
if ($res->successful()) {
return true;
}
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $data['text'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [
@ -70,7 +62,7 @@ class Msteams extends Transport
[ [
'title' => 'Use JSON?', 'title' => 'Use JSON?',
'name' => 'use-json', 'name' => 'use-json',
'descr' => 'Compose MessageCard with JSON rather than Markdown', 'descr' => 'Compose MessageCard with JSON rather than Markdown. Your template must be valid MessageCard JSON',
'type' => 'checkbox', 'type' => 'checkbox',
'default' => false, 'default' => false,
], ],
@ -80,4 +72,48 @@ class Msteams extends Transport
], ],
]; ];
} }
private function messageCard(): string
{
return '{
"@context": "https://schema.org/extensions",
"@type": "MessageCard",
"potentialAction": [
{
"@type": "OpenUri",
"name": "View MessageCard Reference",
"targets": [
{
"os": "default",
"uri": "https://learn.microsoft.com/en-us/outlook/actionable-messages/message-card-reference"
}
]
},
{
"@type": "OpenUri",
"name": "View LibreNMS Website",
"targets": [
{
"os": "default",
"uri": "https://www.librenms.org/"
}
]
}
],
"sections": [
{
"facts": [
{
"name": "Next Action:",
"value": "Make your alert template emit valid MessageCard Json"
}
],
"text": "You have successfully sent a pre-formatted MessageCard message to teams."
}
],
"summary": "Test Successful",
"themeColor": "0072C6",
"title": "Test MessageCard"
}';
}
} }

View File

@ -24,17 +24,11 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Exceptions\AlertTransportDeliveryException;
class Nagios extends Transport class Nagios extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{
$opts = $this->config['nagios-fifo'];
return $this->contactNagios($obj, $opts);
}
public static function contactNagios($obj, $opts)
{ {
/* /*
host_perfdata_file_template= host_perfdata_file_template=
@ -51,20 +45,25 @@ class Nagios extends Transport
$format = ''; $format = '';
$format .= "[HOSTPERFDATA]\t"; $format .= "[HOSTPERFDATA]\t";
$format .= strtotime($obj['timestamp']) . "\t"; $format .= strtotime($alert_data['timestamp']) . "\t";
$format .= $obj['hostname'] . "\t"; $format .= $alert_data['hostname'] . "\t";
$format .= md5($obj['rule']) . "\t"; //FIXME: Better entity $format .= md5($alert_data['rule']) . "\t"; //FIXME: Better entity
$format .= ($obj['state'] ? $obj['severity'] : 'ok') . "\t"; $format .= ($alert_data['state'] ? $alert_data['severity'] : 'ok') . "\t";
$format .= "0\t"; $format .= "0\t";
$format .= "0\t"; $format .= "0\t";
$format .= str_replace("\n", '', nl2br($obj['msg'])) . "\t"; $format .= str_replace("\n", '', nl2br($alert_data['msg'])) . "\t";
$format .= 'NULL'; //FIXME: What's the HOSTPERFDATA equivalent for LibreNMS? Oo $format .= 'NULL'; //FIXME: What's the HOSTPERFDATA equivalent for LibreNMS? Oo
$format .= "\n"; $format .= "\n";
return file_put_contents($opts, $format); $fifo = $this->config['nagios-fifo'];
if (filetype($fifo) !== 'fifo') {
throw new AlertTransportDeliveryException($alert_data, 0, 'File is not a fifo file! Refused to write to it.');
}
return file_put_contents($fifo, $format);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -24,45 +24,25 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Opsgenie extends Transport class Opsgenie extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
if (! empty($this->config)) { $url = $this->config['genie-url'];
$opts['url'] = $this->config['genie-url'];
$res = Http::client()->post($url, $alert_data);
if ($res->successful()) {
return true;
} }
return $this->contactOpsgenie($obj, $opts); throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), '', $alert_data);
} }
public function contactOpsgenie($obj, $opts) public static function configTemplate(): array
{
$url = $opts['url'];
$curl = curl_init();
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($obj));
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200) {
var_dump('Error when sending post request to OpsGenie. Response code: ' . $code . ' Response body: ' . $ret); //FIXME: proper debugging
return false;
}
return true;
}
public static function configTemplate()
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -14,29 +14,20 @@ namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Config; use LibreNMS\Config;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Osticket extends Transport class Osticket extends Transport
{ {
protected $name = 'osTicket'; protected string $name = 'osTicket';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
if (! empty($this->config)) { $url = $this->config['os-url'];
$opts['url'] = $this->config['os-url']; $token = $this->config['os-token'];
$opts['token'] = $this->config['os-token'];
}
return $this->contactOsticket($obj, $opts);
}
public function contactOsticket($obj, $opts)
{
$url = $opts['url'];
$token = $opts['token'];
$email = ''; $email = '';
foreach (parse_email(Config::get('email_from')) as $from => $from_name) { foreach (\LibreNMS\Util\Mail::parseEmails(Config::get('email_from')) as $from => $from_name) {
$email = $from_name . ' <' . $from . '>'; $email = $from_name . ' <' . $from . '>';
break; break;
} }
@ -44,34 +35,24 @@ class Osticket extends Transport
$protocol = [ $protocol = [
'name' => 'LibreNMS', 'name' => 'LibreNMS',
'email' => $email, 'email' => $email,
'subject' => ($obj['name'] ? $obj['name'] . ' on ' . $obj['hostname'] : $obj['title']), 'subject' => ($alert_data['name'] ? $alert_data['name'] . ' on ' . $alert_data['hostname'] : $alert_data['title']),
'message' => strip_tags($obj['msg']), 'message' => strip_tags($alert_data['msg']),
'ip' => $_SERVER['REMOTE_ADDR'], 'ip' => $_SERVER['REMOTE_ADDR'],
'attachments' => [], 'attachments' => [],
]; ];
$curl = curl_init();
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Content-type' => 'application/json',
'Expect:',
'X-API-Key: ' . $token,
]);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($protocol));
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 201) { $res = Http::client()->withHeaders([
var_dump('osTicket returned Error, retry later'); 'X-API-Key' => $token,
])->post($url, $protocol);
return false; if ($res->successful()) {
return true;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $alert_data['msg'], $protocol);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -23,80 +23,55 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Pagerduty extends Transport class Pagerduty extends Transport
{ {
protected $name = 'PagerDuty'; protected string $name = 'PagerDuty';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
if ($obj['state'] == AlertState::RECOVERED) { $event_action = match ($alert_data['state']) {
$obj['event_type'] = 'resolve'; AlertState::RECOVERED => 'resolve',
} elseif ($obj['state'] == AlertState::ACKNOWLEDGED) { AlertState::ACKNOWLEDGED => 'acknowledge',
$obj['event_type'] = 'acknowledge'; default => 'trigger'
} else { };
$obj['event_type'] = 'trigger';
}
return $this->contactPagerduty($obj, $this->config); $safe_message = strip_tags($alert_data['msg']) ?: 'Test';
} $message = array_filter(explode("\n", $safe_message), 'strlen');
/**
* @param array $obj
* @param array $config
* @return bool|string
*/
public function contactPagerduty($obj, $config)
{
$safe_message = strip_tags($obj['msg']) ?: 'Test';
$custom_details = ['message' => array_filter(explode("\n", $safe_message), 'strlen')];
$data = [ $data = [
'routing_key' => $config['service_key'], 'routing_key' => $this->config['service_key'],
'event_action' => $obj['event_type'], 'event_action' => $event_action,
'dedup_key' => (string) $obj['alert_id'], 'dedup_key' => (string) $alert_data['alert_id'],
'payload' => [ 'payload' => [
'custom_details' => $custom_details, 'custom_details' => ['message' => $message],
'group' => (string) \DeviceCache::get($obj['device_id'])->groups->pluck('name'), 'group' => (string) \DeviceCache::get($alert_data['device_id'])->groups->pluck('name'),
'source' => $obj['hostname'], 'source' => $alert_data['hostname'],
'severity' => $obj['severity'], 'severity' => $alert_data['severity'],
'summary' => ($obj['name'] ? $obj['name'] . ' on ' . $obj['hostname'] : $obj['title']), 'summary' => ($alert_data['name'] ? $alert_data['name'] . ' on ' . $alert_data['hostname'] : $alert_data['title']),
], ],
]; ];
// EU service region // EU service region
if ($config['region'] == 'EU') { $url = match ($this->config['region']) {
$url = 'https://events.eu.pagerduty.com/v2/enqueue'; 'EU' => 'https://events.eu.pagerduty.com/v2/enqueue',
} elseif ($config['region'] == 'US') { 'US' => 'https://events.pagerduty.com/v2/enqueue',
// US service region default => $this->config['custom-url'],
$url = 'https://events.pagerduty.com/v2/enqueue'; };
} else {
$url = $config['custom-url']; $res = Http::client()->post($url, $data);
if ($res->successful()) {
return true;
} }
$client = new Client(); throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), implode(PHP_EOL, $message), $data);
$request_opts = ['json' => $data];
$request_opts['proxy'] = Proxy::forGuzzle();
try {
$result = $client->request('POST', $url, $request_opts);
if ($result->getStatusCode() == 202) {
return true;
}
return $result->getReasonPhrase();
} catch (GuzzleException $e) {
return 'Request to PagerDuty API failed. ' . $e->getMessage();
}
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -24,48 +24,40 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Playsms extends Transport class Playsms extends Transport
{ {
protected $name = 'playSMS'; protected string $name = 'playSMS';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$playsms_opts['url'] = $this->config['playsms-url']; $to = preg_split('/([,\r\n]+)/', $this->config['playsms-mobiles']);
$playsms_opts['user'] = $this->config['playsms-user'];
$playsms_opts['token'] = $this->config['playsms-token'];
$playsms_opts['from'] = $this->config['playsms-from'];
$playsms_opts['to'] = preg_split('/([,\r\n]+)/', $this->config['playsms-mobiles']);
return $this->contactPlaysms($obj, $playsms_opts); $url = str_replace('?app=ws', '', $this->config['playsms-url']); // remove old format
} $data = [
'app' => 'ws',
public static function contactPlaysms($obj, $opts) 'op' => 'pv',
{ 'u' => $this->config['playsms-user'],
$data = ['u' => $opts['user'], 'h' => $opts['token'], 'to' => implode(',', $opts['to']), 'msg' => $obj['title']]; 'h' => $this->config['playsms-token'],
if (! empty($opts['from'])) { 'to' => implode(',', $to),
$data['from'] = $opts['from']; 'msg' => $alert_data['title'],
} ];
$url = $opts['url'] . '&op=pv&' . http_build_query($data); if (! empty($this->config['playsms-from'])) {
$curl = curl_init($url); $data['from'] = $this->config['playsms-from'];
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code > 202) {
var_dump($ret);
return;
} }
return true; $res = Http::client()->get($url, $data);
if ($res->successful()) {
return true;
}
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $data['msg'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -24,49 +24,29 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Pushbullet extends Transport class Pushbullet extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{
if (! empty($this->config)) {
$opts = $this->config['pushbullet-token'];
}
return $this->contactPushbullet($obj, $opts);
}
public function contactPushbullet($obj, $opts)
{ {
// Note: At this point it might be useful to iterate through $obj['contacts'] and send each of them a note ? // Note: At this point it might be useful to iterate through $obj['contacts'] and send each of them a note ?
$url = 'https://api.pushbullet.com/v2/pushes';
$data = ['type' => 'note', 'title' => $alert_data['title'], 'body' => $alert_data['msg']];
$data = ['type' => 'note', 'title' => $obj['title'], 'body' => $obj['msg']]; $res = Http::client()
$data = json_encode($data); ->withToken($this->config['pushbullet-token'])
->post($url, $data);
$curl = curl_init('https://api.pushbullet.com/v2/pushes'); if ($res->successful()) {
Proxy::applyToCurl($curl); return true;
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($data),
'Authorization: Bearer ' . $opts,
]);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code > 201) {
var_dump($ret);
return 'HTTP Status code ' . $code;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $alert_data['msg'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -39,68 +39,57 @@ namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Pushover extends Transport class Pushover extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$pushover_opts = $this->config; $options = $this->parseUserOptions($this->config['options']);
$pushover_opts['options'] = $this->parseUserOptions($this->config['options']);
return $this->contactPushover($obj, $pushover_opts); $url = 'https://api.pushover.net/1/messages.json';
}
public function contactPushover($obj, $api)
{
$data = []; $data = [];
$data['token'] = $api['appkey']; $data['token'] = $this->config['appkey'];
$data['user'] = $api['userkey']; $data['user'] = $this->config['userkey'];
switch ($obj['severity']) { switch ($alert_data['severity']) {
case 'critical': case 'critical':
$data['priority'] = 1; $data['priority'] = 1;
if (! empty($api['options']['sound_critical'])) { if (! empty($options['sound_critical'])) {
$data['sound'] = $api['options']['sound_critical']; $data['sound'] = $options['sound_critical'];
} }
break; break;
case 'warning': case 'warning':
$data['priority'] = 1; $data['priority'] = 1;
if (! empty($api['options']['sound_warning'])) { if (! empty($options['sound_warning'])) {
$data['sound'] = $api['options']['sound_warning']; $data['sound'] = $options['sound_warning'];
} }
break; break;
} }
switch ($obj['state']) { switch ($alert_data['state']) {
case AlertState::RECOVERED: case AlertState::RECOVERED:
$data['priority'] = 0; $data['priority'] = 0;
if (! empty($api['options']['sound_ok'])) { if (! empty($options['sound_ok'])) {
$data['sound'] = $api['options']['sound_ok']; $data['sound'] = $options['sound_ok'];
} }
break; break;
} }
$data['title'] = $obj['title']; $data['title'] = $alert_data['title'];
$data['message'] = $obj['msg']; $data['message'] = $alert_data['msg'];
if ($api['options']) { if ($options) {
$data = array_merge($data, $api['options']); $data = array_merge($data, $options);
}
$curl = curl_init();
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_URL, 'https://api.pushover.net/1/messages.json');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_SAFE_UPLOAD, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200) {
var_dump('Pushover returned error'); //FIXME: proper debugging
return 'HTTP Status code ' . $code;
} }
return true; $res = Http::client()->asForm()->post($url, $data);
if ($res->successful()) {
return true;
}
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $data['message'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -24,62 +24,43 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Rocket extends Transport class Rocket extends Transport
{ {
protected $name = 'Rocket Chat'; protected string $name = 'Rocket Chat';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$rocket_opts = $this->parseUserOptions($this->config['rocket-options']); $rocket_opts = $this->parseUserOptions($this->config['rocket-options']);
$rocket_opts['url'] = $this->config['rocket-url'];
return $this->contactRocket($obj, $rocket_opts); $rocket_msg = strip_tags($alert_data['msg']);
}
public static function contactRocket($obj, $api)
{
$host = $api['url'];
$curl = curl_init();
$rocket_msg = strip_tags($obj['msg']);
$color = self::getColorForState($obj['state']);
$data = [ $data = [
'attachments' => [ 'attachments' => [
0 => [ 0 => [
'fallback' => $rocket_msg, 'fallback' => $rocket_msg,
'color' => $color, 'color' => self::getColorForState($alert_data['state']),
'title' => $obj['title'], 'title' => $alert_data['title'],
'text' => $rocket_msg, 'text' => $rocket_msg,
], ],
], ],
'channel' => $api['channel'] ?? null, 'channel' => $rocket_opts['channel'] ?? null,
'username' => $api['username'] ?? null, 'username' => $rocket_opts['username'] ?? null,
'icon_url' => $api['icon_url'] ?? null, 'icon_url' => $rocket_opts['icon_url'] ?? null,
'icon_emoji' => $api['icon_emoji'] ?? null, 'icon_emoji' => $rocket_opts['icon_emoji'] ?? null,
]; ];
$alert_message = json_encode($data);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_URL, $host);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $alert_message);
$ret = curl_exec($curl); $res = Http::client()->post($this->config['rocket-url'], $data);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200) {
var_dump("API '$host' returned Error"); //FIXME: propper debuging
var_dump('Params: ' . $alert_message); //FIXME: propper debuging
var_dump('Return: ' . $ret); //FIXME: propper debuging
return 'HTTP Status code ' . $code; if ($res->successful()) {
return true;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $rocket_msg, $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -22,13 +22,12 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Config; use LibreNMS\Config;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Sensu extends Transport class Sensu extends Transport
{ {
@ -38,152 +37,120 @@ class Sensu extends Transport
public const CRITICAL = 2; public const CRITICAL = 2;
public const UNKNOWN = 3; public const UNKNOWN = 3;
private static $status = [ private static array $status = [
'ok' => Sensu::OK, 'ok' => self::OK,
'warning' => Sensu::WARNING, 'warning' => self::WARNING,
'critical' => Sensu::CRITICAL, 'critical' => self::CRITICAL,
]; ];
private static $severity = [ public function deliverAlert(array $alert_data): bool
'recovered' => AlertState::RECOVERED,
'alert' => AlertState::ACTIVE,
'acknowledged' => AlertState::ACKNOWLEDGED,
'worse' => AlertState::WORSE,
'better' => AlertState::BETTER,
];
/**
* @var Client
*/
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';
$sensu_opts['namespace'] = $this->config['sensu-namespace'] ? $this->config['sensu-namespace'] : 'default';
$sensu_opts['prefix'] = $this->config['sensu-prefix'];
$sensu_opts['source-key'] = $this->config['sensu-source-key']; $sensu_opts['source-key'] = $this->config['sensu-source-key'];
Sensu::$client = new Client(); $url = $this->config['sensu-url'] ?: 'http://127.0.0.1:3031';
$client = Http::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 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 // The agent API is documented at https://docs.sensu.io/sensu-go/latest/reference/agent/#create-monitoring-events-using-the-agent-api
$request_options = ['proxy' => Proxy::forGuzzle()];
if (Sensu::$client->request('GET', $opts['url'] . '/healthz', $request_options)->getStatusCode() !== 200) { $health_check = $client->get($url . '/healthz')->status();
return 'Sensu API is not responding'; if ($health_check !== 200) {
throw new AlertTransportDeliveryException($alert_data, $health_check, 'Sensu API is not responding');
} }
if ($obj['state'] !== AlertState::RECOVERED && $obj['state'] !== AlertState::ACKNOWLEDGED && $obj['alerted'] === 0) { if ($alert_data['state'] !== AlertState::RECOVERED && $alert_data['state'] !== AlertState::ACKNOWLEDGED && $alert_data['alerted'] === 0) {
// 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 // 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)); $data = $this->generateData($alert_data, self::OK, (int) round(Config::get('rrd.step', 300) / 2));
Log::debug('Sensu transport sent last good event to socket: ', $data); Log::debug('Sensu transport sent last good event to socket: ', $data);
$request_options['json'] = $data; $result = $client->post($url . '/events', $data);
$result = Sensu::$client->request('POST', $opts['url'] . '/events', $request_options); if ($result->status() !== 202) {
if ($result->getStatusCode() !== 202) { throw new AlertTransportDeliveryException($alert_data, $result->status(), $result->body(), json_encode($data), $this->config);
return $result->getReasonPhrase();
} }
sleep(5); sleep(5);
} }
$data = Sensu::generateData($obj, $opts, Sensu::calculateStatus($obj['state'], $obj['severity'])); $data = $this->generateData($alert_data, $this->calculateStatus($alert_data['state'], $alert_data['severity']));
Log::debug('Sensu transport sent event to socket: ', $data);
$request_options['json'] = $data; $result = $client->post($url . '/events', $data);
$result = Sensu::$client->request('POST', $opts['url'] . '/events', $request_options);
if ($result->getStatusCode() === 202) { if ($result->successful()) {
return true; return true;
} }
return $result->getReasonPhrase(); throw new AlertTransportDeliveryException($alert_data, $result->status(), $result->body(), json_encode($data), $sensu_opts);
} }
public static function generateData($obj, $opts, $status, $offset = 0) private function generateData(array $alert_data, int $status, int $offset = 0): array
{ {
$namespace = $this->config['sensu-namespace'] ?: 'default';
return [ return [
'check' => [ 'check' => [
'metadata' => [ 'metadata' => [
'name' => Sensu::checkName($opts['prefix'], $obj['name']), 'name' => $this->checkName($this->config['sensu-prefix'], $alert_data['name']),
'namespace' => $opts['namespace'], 'namespace' => $namespace,
'annotations' => Sensu::generateAnnotations($obj), 'annotations' => $this->generateAnnotations($alert_data),
], ],
'command' => sprintf('LibreNMS: %s', $obj['builder']), 'command' => sprintf('LibreNMS: %s', $alert_data['builder']),
'executed' => time() - $offset, 'executed' => time() - $offset,
'interval' => Config::get('rrd.step', 300), 'interval' => Config::get('rrd.step', 300),
'issued' => time() - $offset, 'issued' => time() - $offset,
'output' => $obj['msg'], 'output' => $alert_data['msg'],
'status' => $status, 'status' => $status,
], ],
'entity' => [ 'entity' => [
'metadata' => [ 'metadata' => [
'name' => Sensu::getEntityName($obj, $opts['source-key']), 'name' => $this->getEntityName($alert_data),
'namespace' => $opts['namespace'], 'namespace' => $namespace,
], ],
'system' => [ 'system' => [
'hostname' => $obj['hostname'], 'hostname' => $alert_data['hostname'],
'os' => $obj['os'], 'os' => $alert_data['os'],
], ],
], ],
]; ];
} }
public static function generateAnnotations($obj) private function generateAnnotations(array $alert_data): array
{ {
return array_filter([ return array_filter([
'generated-by' => 'LibreNMS', 'generated-by' => 'LibreNMS',
'acknowledged' => $obj['state'] === AlertState::ACKNOWLEDGED ? 'true' : 'false', 'acknowledged' => $alert_data['state'] === AlertState::ACKNOWLEDGED ? 'true' : 'false',
'contact' => $obj['sysContact'], 'contact' => $alert_data['sysContact'],
'description' => $obj['sysDescr'], 'description' => $alert_data['sysDescr'],
'location' => $obj['location'], 'location' => $alert_data['location'],
'documentation' => $obj['proc'], 'documentation' => $alert_data['proc'],
'librenms-notes' => $obj['notes'], 'librenms-notes' => $alert_data['notes'],
'librenms-device-id' => strval($obj['device_id']), 'librenms-device-id' => strval($alert_data['device_id']),
'librenms-rule-id' => strval($obj['rule_id']), 'librenms-rule-id' => strval($alert_data['rule_id']),
'librenms-status-reason' => $obj['status_reason'], 'librenms-status-reason' => $alert_data['status_reason'],
], function (?string $s): bool { ], function (?string $s): bool {
return (bool) strlen($s); // strlen returns 0 for null, false or '', but 1 for integer 0 - unlike empty() return (bool) strlen($s); // strlen returns 0 for null, false or '', but 1 for integer 0 - unlike empty()
}); });
} }
public static function calculateStatus($state, $severity) private function calculateStatus(int $state, string $severity): int
{ {
// Sensu only has a single short (status) to indicate both severity and status, so we need to map LibreNMS' state and severity onto it // Sensu only has a single short (status) to indicate both severity and status, so we need to map LibreNMS' state and severity onto it
if ($state === AlertState::RECOVERED) { if ($state === AlertState::RECOVERED) {
// LibreNMS alert is resolved, send ok // LibreNMS alert is resolved, send ok
return Sensu::OK; return self::OK;
} }
if (array_key_exists($severity, Sensu::$status)) { return self::$status[$severity] ?? self::UNKNOWN;
// 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) private function getEntityName(array $obj): string
{ {
if ($key === 'shortname') { $key = $this->config['sensu-source-key'] ?: 'display';
return Sensu::shortenName($obj['display']);
}
return $obj[$key]; return $key === 'shortname' ? $this->shortenName($obj['display']) : $obj[$key];
} }
public static function shortenName($name) private function shortenName(string $name): string
{ {
// Shrink the last domain components - e.g. librenms.corp.example.net becomes librenms.cen // Shrink the last domain components - e.g. librenms.corp.example.net becomes librenms.cen
$components = explode('.', $name); $components = explode('.', $name);
@ -204,7 +171,7 @@ class Sensu extends Transport
return sprintf('%s.%s', implode('.', $components), $result); return sprintf('%s.%s', implode('.', $components), $result);
} }
public static function checkName($prefix, $name) private function checkName(string $prefix, string $name): string
{ {
$check = strtolower(str_replace(' ', '-', $name)); $check = strtolower(str_replace(' ', '-', $name));
@ -215,7 +182,7 @@ class Sensu extends Transport
return $check; return $check;
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -26,29 +26,18 @@ use LibreNMS\Alert\Transport;
class Signal extends Transport class Signal extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$signalOpts = [ exec(escapeshellarg($this->config['path'])
'path' => escapeshellarg($this->config['path']),
'recipient-type' => ($this->config['recipient-type'] == 'group') ? ' -g ' : ' ',
'recipient' => escapeshellarg($this->config['recipient']),
];
return $this->contactSignal($obj, $signalOpts);
}
public function contactSignal($obj, $opts)
{
exec($opts['path']
. ' --dbus-system send' . ' --dbus-system send'
. $opts['recipient-type'] . (($this->config['recipient-type'] == 'group') ? ' -g ' : ' ')
. $opts['recipient'] . escapeshellarg($this->config['recipient'])
. ' -m ' . escapeshellarg($obj['title'])); . ' -m ' . escapeshellarg($alert_data['title']));
return true; return true;
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'validation' => [], 'validation' => [],

View File

@ -18,62 +18,35 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Signalwire extends Transport class Signalwire extends Transport
{ {
protected $name = 'SignalWire'; protected string $name = 'SignalWire';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$signalwire_opts['spaceUrl'] = $this->config['signalwire-spaceUrl']; $url = 'https://' . $this->config['signalwire-spaceUrl'] . '.signalwire.com/api/laml/2010-04-01/Accounts/' . $this->config['signalwire-project-id'] . '/Messages.json';
$signalwire_opts['sid'] = $this->config['signalwire-project-id'];
$signalwire_opts['token'] = $this->config['signalwire-token'];
$signalwire_opts['sender'] = $this->config['signalwire-sender'];
$signalwire_opts['to'] = $this->config['signalwire-to'];
return $this->contactSignalwire($obj, $signalwire_opts);
}
public static function contactSignalwire($obj, $opts)
{
$params = [
'spaceUrl' => $opts['spaceUrl'],
'sid' => $opts['sid'],
'token' => $opts['token'],
'phone' => $opts['to'],
'text' => $obj['title'],
'sender' => $opts['sender'],
];
$url = 'https://' . $params['spaceUrl'] . '.signalwire.com/api/laml/2010-04-01/Accounts/' . $params['sid'] . '/Messages.json';
$data = [ $data = [
'From' => $params['sender'], 'From' => $this->config['signalwire-sender'],
'Body' => $params['text'], 'To' => $this->config['signalwire-to'],
'To' => $params['phone'], 'Body' => $alert_data['title'],
]; ];
$post = http_build_query($data);
$curl = curl_init($url); $res = Http::client()->asForm()
->withBasicAuth($this->config['signalwire-project-id'], $this->config['signalwire-token'])
->post($url, $data);
Proxy::applyToCurl($curl); if ($res->successful()) {
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($curl, CURLOPT_USERPWD, $params['sid'] . ':' . $params['token']);
curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
curl_exec($curl);
if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE)) {
return true; return true;
} }
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $alert_data['title'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -23,62 +23,44 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use Illuminate\Support\Str;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Slack extends Transport class Slack extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$slack_opts = $this->parseUserOptions($this->config['slack-options']); $slack_opts = $this->parseUserOptions($this->config['slack-options'] ?? '');
$slack_opts['url'] = $this->config['slack-url']; $icon = $this->config['slack-icon_emoji'] ?? $slack_opts['icon_emoji'] ?? null;
$slack_msg = html_entity_decode(strip_tags($alert_data['msg'] ?? ''), ENT_QUOTES);
return $this->contactSlack($obj, $slack_opts);
}
public static function contactSlack($obj, $api)
{
$host = $api['url'];
$curl = curl_init();
$slack_msg = html_entity_decode(strip_tags($obj['msg'] ?? ''), ENT_QUOTES);
$color = self::getColorForState($obj['state']);
$data = [ $data = [
'attachments' => [ 'attachments' => [
0 => [ 0 => [
'fallback' => $slack_msg, 'fallback' => $slack_msg,
'color' => $color, 'color' => self::getColorForState($alert_data['state']),
'title' => $obj['title'] ?? null, 'title' => $alert_data['title'] ?? null,
'text' => $slack_msg, 'text' => $slack_msg,
'mrkdwn_in' => ['text', 'fallback'], 'mrkdwn_in' => ['text', 'fallback'],
'author_name' => $api['author_name'] ?? null, 'author_name' => $this->config['slack-author'] ?? $slack_opts['author'] ?? null,
], ],
], ],
'channel' => $api['channel'] ?? null, 'channel' => $this->config['slack-channel'] ?? $slack_opts['channel'] ?? null,
'username' => $api['username'] ?? null, 'icon_emoji' => $icon ? Str::finish(Str::start($icon, ':'), ':') : null,
'icon_emoji' => isset($api['icon_emoji']) ? ':' . $api['icon_emoji'] . ':' : null,
]; ];
$alert_message = json_encode($data);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_URL, $host);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $alert_message);
$ret = curl_exec($curl); $res = Http::client()->post($this->config['slack-url'] ?? '', $data);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200) {
var_dump("API '$host' returned Error"); //FIXME: propper debuging
var_dump('Params: ' . $alert_message); //FIXME: propper debuging
var_dump('Return: ' . $ret); //FIXME: propper debuging
return 'HTTP Status code ' . $code; if ($res->successful()) {
return true;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $slack_msg, $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [
@ -89,14 +71,30 @@ class Slack extends Transport
'type' => 'text', 'type' => 'text',
], ],
[ [
'title' => 'Slack Options', 'title' => 'Channel',
'name' => 'slack-options', 'name' => 'slack-channel',
'descr' => 'Slack Options', 'descr' => 'Channel to post to',
'type' => 'textarea', 'type' => 'text',
],
[
'title' => 'Author Name',
'name' => 'slack-author',
'descr' => 'Name of author',
'type' => 'text',
'default' => 'LibreNMS',
],
[
'title' => 'Icon',
'name' => 'slack-author',
'descr' => 'Name of emoji for icon',
'type' => 'text',
], ],
], ],
'validation' => [ 'validation' => [
'slack-url' => 'required|url', 'slack-url' => 'required|url',
'slack-channel' => 'string',
'slack-username' => 'string',
'slack-icon_emoji' => 'string',
], ],
]; ];
} }

View File

@ -23,49 +23,44 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use Illuminate\Support\Str;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Smseagle extends Transport class Smseagle extends Transport
{ {
protected $name = 'SMSEagle'; protected string $name = 'SMSEagle';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$smseagle_opts['url'] = $this->config['smseagle-url']; $url = $this->config['smseagle-url'] . '/http_api/send_sms';
$smseagle_opts['user'] = $this->config['smseagle-user']; if (! str_starts_with($url, 'http')) {
$smseagle_opts['token'] = $this->config['smseagle-pass']; $url = 'http://' . $url;
$smseagle_opts['to'] = preg_split('/([,\r\n]+)/', $this->config['smseagle-mobiles']);
return $this->contactSmseagle($obj, $smseagle_opts);
}
public static function contactSmseagle($obj, $opts)
{
$params = [
'login' => $opts['user'],
'pass' => $opts['token'],
'to' => implode(',', $opts['to']),
'message' => $obj['title'],
];
$url = Str::startsWith($opts['url'], 'http') ? '' : 'http://';
$url .= $opts['url'] . '/index.php/http_api/send_sms?' . http_build_query($params);
$curl = curl_init($url);
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$ret = curl_exec($curl);
if (substr($ret, 0, 2) == 'OK') {
return true;
} else {
return false;
} }
$params = [];
// use token if available
if (empty($this->config['smseagle-token'])) {
$params['login'] = $this->config['smseagle-user'];
$params['pass'] = $this->config['smseagle-pass'];
} else {
$params['access_token'] = $this->config['smseagle-token'];
}
$params['to'] = implode(',', preg_split('/([,\r\n]+)/', $this->config['smseagle-mobiles']));
$params['message'] = $alert_data['title'];
$res = Http::client()->get($url, $params);
if ($res->successful() && str_starts_with($res->body(), 'OK')) {
return true;
}
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $params['message'], $params);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [
@ -75,6 +70,12 @@ class Smseagle extends Transport
'descr' => 'SMSEagle Host', 'descr' => 'SMSEagle Host',
'type' => 'text', 'type' => 'text',
], ],
[
'title' => 'Access Token',
'name' => 'smseagle-token',
'descr' => 'SMSEagle Access Token',
'type' => 'text',
],
[ [
'title' => 'User', 'title' => 'User',
'name' => 'smseagle-user', 'name' => 'smseagle-user',
@ -96,8 +97,9 @@ class Smseagle extends Transport
], ],
'validation' => [ 'validation' => [
'smseagle-url' => 'required|url', 'smseagle-url' => 'required|url',
'smseagle-user' => 'required|string', 'smseagle-token' => 'required_without:smseagle-user,smseagle-pass|string',
'smseagle-pass' => 'required|string', 'smseagle-user' => 'required_without:smseagle-token|string',
'smseagle-pass' => 'required_without:smseagle-token|string',
'smseagle-mobiles' => 'required', 'smseagle-mobiles' => 'required',
], ],
]; ];

View File

@ -24,45 +24,34 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Smsfeedback extends Transport class Smsfeedback extends Transport
{ {
protected $name = 'SMSfeedback'; protected string $name = 'SMSfeedback';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{
$smsfeedback_opts['user'] = $this->config['smsfeedback-user'];
$smsfeedback_opts['token'] = $this->config['smsfeedback-pass'];
$smsfeedback_opts['sender'] = $this->config['smsfeedback-sender'];
$smsfeedback_opts['to'] = $this->config['smsfeedback-mobiles'];
return $this->contactsmsfeedback($obj, $smsfeedback_opts);
}
public static function contactsmsfeedback($obj, $opts)
{ {
$url = 'http://api.smsfeedback.ru/messages/v2/send/';
$params = [ $params = [
'login' => $opts['user'], 'phone' => $this->config['smsfeedback-mobiles'],
'pass' => md5($opts['token']), 'text' => $alert_data['title'],
'phone' => $opts['to'], 'sender' => $this->config['smsfeedback-sender'],
'text' => $obj['title'],
'sender' => $opts['sender'],
]; ];
$url = 'http://' . $opts['user'] . ':' . $opts['token'] . '@' . 'api.smsfeedback.ru/messages/v2/send/?' . http_build_query($params);
$curl = curl_init($url);
Proxy::applyToCurl($curl); $res = Http::client()
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET'); ->withBasicAuth($this->config['smsfeedback-user'], $this->config['smsfeedback-pass'])
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); ->get($url, $params);
$ret = curl_exec($curl); if ($res->successful() && str_starts_with($res->body(), 'accepted')) {
if (substr($ret, 0, 8) == 'accepted') {
return true; return true;
} }
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $alert_data['title'], $params);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [
@ -95,7 +84,7 @@ class Smsfeedback extends Transport
'smsfeedback-user' => 'required|string', 'smsfeedback-user' => 'required|string',
'smsfeedback-pass' => 'required|string', 'smsfeedback-pass' => 'required|string',
'smsfeedback-mobiles' => 'required', 'smsfeedback-mobiles' => 'required',
'smsfeedback-sender' => 'required|string', 'smsfeedback-sender' => 'required|string',
], ],
]; ];
} }

View File

@ -19,51 +19,18 @@ namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
use LibreNMS\Exceptions\AlertTransportDeliveryException;
class Splunk extends Transport class Splunk extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
if (! empty($this->config)) { $splunk_host = empty($this->config['Splunk-host']) ? '127.0.0.1' : $this->config['Splunk-host'];
$opts['splunk_host'] = $this->config['Splunk-host']; $splunk_port = empty($this->config['Splunk-port']) ? 514 : $this->config['Splunk-port'];
$opts['splunk_port'] = $this->config['Splunk-port'];
}
return $this->contactSplunk($obj, $opts);
}
public function contactSplunk($obj, $opts)
{
$splunk_host = '127.0.0.1';
$splunk_port = 514;
$severity = 6; // Default severity is 6 (Informational) $severity = 6; // Default severity is 6 (Informational)
$device = device_by_id_cache($obj['device_id']); // for event logging $device = device_by_id_cache($alert_data['device_id']); // for event logging
if (! empty($opts['splunk_host'])) { switch ($alert_data['severity']) {
if (preg_match('/[a-zA-Z]/', $opts['splunk_host'])) {
$splunk_host = gethostbyname($opts['splunk_host']);
if ($splunk_host === $opts['splunk_host']) {
log_event('Alphanumeric hostname found but does not resolve to an IP.', $device, 'poller', 5);
return false;
}
} elseif (filter_var($opts['splunk_host'], FILTER_VALIDATE_IP)) {
$splunk_host = $opts['splunk_host'];
} else {
log_event('Splunk host is not a valid IP: ' . $opts['splunk_host'], $device, 'poller', 5);
return false;
}
} else {
log_event('Splunk host is empty.', $device, 'poller');
}
if (! empty($opts['splunk_port']) && preg_match("/^\d+$/", $opts['splunk_port'])) {
$splunk_port = $opts['splunk_port'];
} else {
log_event('Splunk port is not an integer.', $device, 'poller', 5);
}
switch ($obj['severity']) {
case 'critical': case 'critical':
$severity = 2; $severity = 2;
break; break;
@ -72,7 +39,7 @@ class Splunk extends Transport
break; break;
} }
switch ($obj['state']) { switch ($alert_data['state']) {
case AlertState::RECOVERED: case AlertState::RECOVERED:
$severity = 6; $severity = 6;
break; break;
@ -83,7 +50,7 @@ class Splunk extends Transport
$ignore = ['template', 'contacts', 'rule', 'string', 'debug', 'faults', 'builder', 'transport', 'alert', 'msg', 'transport_name']; $ignore = ['template', 'contacts', 'rule', 'string', 'debug', 'faults', 'builder', 'transport', 'alert', 'msg', 'transport_name'];
$splunk_prefix = '<' . $severity . '> '; $splunk_prefix = '<' . $severity . '> ';
foreach ($obj as $key => $val) { foreach ($alert_data as $key => $val) {
if (in_array($key, $ignore)) { if (in_array($key, $ignore)) {
continue; continue;
} }
@ -102,26 +69,24 @@ class Splunk extends Transport
$splunk_prefix = substr($splunk_prefix, 0, -1); $splunk_prefix = substr($splunk_prefix, 0, -1);
if (($socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP)) === false) { if (($socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP)) === false) {
log_event('socket_create() failed: reason: ' . socket_strerror(socket_last_error()), $device, 'poller', 5); throw new AlertTransportDeliveryException($alert_data, 0, 'socket_create() failed: reason: ' . socket_strerror(socket_last_error()));
}
return false; if (! empty($alert_data['faults'])) {
} else { foreach ($alert_data['faults'] as $fault) {
if (! empty($obj['faults'])) { $splunk_msg = $splunk_prefix . ' - ' . $fault['string'];
foreach ($obj['faults'] as $k => $v) {
$splunk_msg = $splunk_prefix . ' - ' . $v['string'];
socket_sendto($socket, $splunk_msg, strlen($splunk_msg), 0, $splunk_host, $splunk_port);
}
} else {
$splunk_msg = $splunk_prefix;
socket_sendto($socket, $splunk_msg, strlen($splunk_msg), 0, $splunk_host, $splunk_port); socket_sendto($socket, $splunk_msg, strlen($splunk_msg), 0, $splunk_host, $splunk_port);
} }
socket_close($socket); } else {
$splunk_msg = $splunk_prefix;
socket_sendto($socket, $splunk_msg, strlen($splunk_msg), 0, $splunk_host, $splunk_port);
} }
socket_close($socket);
return true; return true;
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [
@ -130,17 +95,19 @@ class Splunk extends Transport
'name' => 'Splunk-host', 'name' => 'Splunk-host',
'descr' => 'Splunk Host', 'descr' => 'Splunk Host',
'type' => 'text', 'type' => 'text',
'default' => '127.0.0.1',
], ],
[ [
'title' => 'UDP Port', 'title' => 'UDP Port',
'name' => 'Splunk-port', 'name' => 'Splunk-port',
'descr' => 'Splunk Port', 'descr' => 'Splunk Port',
'type' => 'text', 'type' => 'text',
'default' => 514,
], ],
], ],
'validation' => [ 'validation' => [
'Splunk-host' => 'required|string', 'Splunk-host' => 'required|ip_or_hostname',
'Splunk-port' => 'required|numeric', 'Splunk-port' => 'integer|between:1,65536',
], ],
]; ];
} }

View File

@ -19,21 +19,16 @@ namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
use LibreNMS\Exceptions\AlertTransportDeliveryException;
class Syslog extends Transport class Syslog extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
if (! empty($this->config)) { return $this->contactSyslog($alert_data);
$opts['syslog_host'] = $this->config['syslog-host'];
$opts['syslog_port'] = $this->config['syslog-port'];
$opts['syslog_facility'] = $this->config['syslog-facility'];
}
return $this->contactSyslog($obj, $opts);
} }
public function contactSyslog($obj, $opts) public function contactSyslog(array $alert_data): bool
{ {
$syslog_host = '127.0.0.1'; $syslog_host = '127.0.0.1';
$syslog_port = 514; $syslog_port = 514;
@ -41,38 +36,26 @@ class Syslog extends Transport
$facility = 24; // Default facility is 3 * 8 (daemon) $facility = 24; // Default facility is 3 * 8 (daemon)
$severity = 6; // Default severity is 6 (Informational) $severity = 6; // Default severity is 6 (Informational)
$sev_txt = 'OK'; $sev_txt = 'OK';
$device = device_by_id_cache($obj['device_id']); // for event logging
if (! empty($opts['syslog_facility']) && preg_match("/^\d+$/", $opts['syslog_facility'])) { if (! empty($this->config['syslog-facility'])) {
$facility = (int) $opts['syslog_facility'] * 8; $facility = (int) $this->config['syslog-facility'] * 8;
} else {
log_event('Syslog facility is not an integer: ' . $opts['syslog_facility'], $device, 'poller', 5);
} }
if (! empty($opts['syslog_host'])) {
if (preg_match('/[a-zA-Z]/', $opts['syslog_host'])) {
$syslog_host = gethostbyname($opts['syslog_host']);
if ($syslog_host === $opts['syslog_host']) {
log_event('Alphanumeric hostname found but does not resolve to an IP.', $device, 'poller', 5);
return false; if (! empty($this->config['syslog-host'])) {
if (preg_match('/[a-zA-Z]/', $this->config['syslog-host'])) {
$syslog_host = gethostbyname($this->config['syslog-host']);
if ($syslog_host === $this->config['syslog-host']) {
throw new AlertTransportDeliveryException($alert_data, 0, 'Hostname found but does not resolve to an IP.');
} }
} elseif (filter_var($opts['syslog_host'], FILTER_VALIDATE_IP)) {
$syslog_host = $opts['syslog_host'];
} else {
log_event('Syslog host is not a valid IP: ' . $opts['syslog_host'], $device, 'poller', 5);
return false;
} }
} else { $syslog_host = $this->config['syslog-host'];
log_event('Syslog host is empty.', $device, 'poller');
}
if (! empty($opts['syslog_port']) && preg_match("/^\d+$/", $opts['syslog_port'])) {
$syslog_port = $opts['syslog_port'];
} else {
log_event('Syslog port is not an integer.', $device, 'poller', 5);
} }
switch ($obj['severity']) { if (! empty($this->config['syslog-port'])) {
$syslog_port = $this->config['syslog-port'];
}
switch ($alert_data['severity']) {
case 'critical': case 'critical':
$severity = 2; $severity = 2;
$sev_txt = 'Critical'; $sev_txt = 'Critical';
@ -83,7 +66,7 @@ class Syslog extends Transport
break; break;
} }
switch ($obj['state']) { switch ($alert_data['state']) {
case AlertState::RECOVERED: case AlertState::RECOVERED:
$state = 'OK'; $state = 'OK';
$severity = 6; $severity = 6;
@ -106,35 +89,33 @@ class Syslog extends Transport
. gethostname() . gethostname()
. ' librenms' . ' librenms'
. '[' . '['
. $obj['device_id'] . $alert_data['device_id']
. ']: ' . ']: '
. $obj['hostname'] . $alert_data['hostname']
. ': [' . ': ['
. $state . $state
. '] ' . '] '
. $obj['name']; . $alert_data['name'];
if (($socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP)) === false) { if (($socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP)) === false) {
log_event('socket_create() failed: reason: ' . socket_strerror(socket_last_error()), $device, 'poller', 5); throw new AlertTransportDeliveryException($alert_data, 0, 'socket_create() failed: reason: ' . socket_strerror(socket_last_error()));
}
return false; if (! empty($alert_data['faults'])) {
} else { foreach ($alert_data['faults'] as $fault) {
if (! empty($obj['faults'])) { $syslog_msg = $syslog_prefix . ' - ' . $fault['string'];
foreach ($obj['faults'] as $k => $v) {
$syslog_msg = $syslog_prefix . ' - ' . $v['string'];
socket_sendto($socket, $syslog_msg, strlen($syslog_msg), 0, $syslog_host, $syslog_port);
}
} else {
$syslog_msg = $syslog_prefix;
socket_sendto($socket, $syslog_msg, strlen($syslog_msg), 0, $syslog_host, $syslog_port); socket_sendto($socket, $syslog_msg, strlen($syslog_msg), 0, $syslog_host, $syslog_port);
} }
socket_close($socket); } else {
$syslog_msg = $syslog_prefix;
socket_sendto($socket, $syslog_msg, strlen($syslog_msg), 0, $syslog_host, $syslog_port);
} }
socket_close($socket);
return true; return true;
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [
@ -158,9 +139,9 @@ class Syslog extends Transport
], ],
], ],
'validation' => [ 'validation' => [
'syslog-host' => 'required|string', 'syslog-host' => 'required|ip_or_hostname',
'syslog-port' => 'required|numeric', 'syslog-port' => 'required|integer|between:1,65536',
'syslog-facility' => 'required|string', 'syslog-facility' => 'required|integer|between:0,23',
], ],
]; ];
} }

View File

@ -26,51 +26,42 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Telegram extends Transport class Telegram extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$telegram_opts['chat_id'] = $this->config['telegram-chat-id']; $url = "https://api.telegram.org/bot{$this->config['telegram-token']}/sendMessage";
$telegram_opts['message_thread_id'] = $this->config['message-thread-id'] ?? null; $format = $this->config['telegram-format'];
$telegram_opts['token'] = $this->config['telegram-token']; $text = $format == 'Markdown'
$telegram_opts['format'] = $this->config['telegram-format']; ? preg_replace('/([a-z0-9]+)_([a-z0-9]+)/', "$1\_$2", $alert_data['msg'])
: $alert_data['msg'];
return $this->contactTelegram($obj, $telegram_opts); $params = [
'chat_id' => $this->config['telegram-chat-id'],
'text' => $text,
];
if ($format) {
$params['format'] = $this->config['telegram-format'];
}
if (! empty($this->config['message-thread-id'])) {
$params['message_thread_id'] = $this->config['message-thread-id'];
}
$res = Http::client()->get($url, $params);
if ($res->successful()) {
return true;
}
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $text, $params);
} }
public static function contactTelegram($obj, $data) public static function configTemplate(): array
{
$curl = curl_init();
Proxy::applyToCurl($curl);
$text = urlencode($obj['msg']);
$format = '';
if ($data['format']) {
$format = '&parse_mode=' . $data['format'];
if ($data['format'] == 'Markdown') {
$text = urlencode(preg_replace('/([a-z0-9]+)_([a-z0-9]+)/', "$1\_$2", $obj['msg']));
}
}
$messageThreadId = '';
if (! empty($data['message_thread_id'])) {
$messageThreadId = '&message_thread_id=' . $data['message_thread_id'];
}
curl_setopt($curl, CURLOPT_URL, "https://api.telegram.org/bot{$data['token']}/sendMessage?chat_id={$data['chat_id']}$messageThreadId&text=$text{$format}");
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200) {
var_dump('Telegram returned Error'); //FIXME: propper debuging
var_dump('Return: ' . $ret); //FIXME: propper debuging
return 'HTTP Status code ' . $code . ', Body ' . $ret;
}
return true;
}
public static function configTemplate()
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -16,58 +16,33 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Twilio extends Transport class Twilio extends Transport
{ {
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
$twilio_opts['sid'] = $this->config['twilio-sid']; $url = 'https://api.twilio.com/2010-04-01/Accounts/' . $this->config['twilio-sid'] . '/Messages.json';
$twilio_opts['token'] = $this->config['twilio-token'];
$twilio_opts['sender'] = $this->config['twilio-sender'];
$twilio_opts['to'] = $this->config['twilio-to'];
return $this->contactTwilio($obj, $twilio_opts);
}
public static function contactTwilio($obj, $opts)
{
$params = [
'sid' => $opts['sid'],
'token' => $opts['token'],
'phone' => $opts['to'],
'text' => $obj['msg'],
'sender' => $opts['sender'],
];
$url = 'https://api.twilio.com/2010-04-01/Accounts/' . $params['sid'] . '/Messages.json';
$data = [ $data = [
'From' => $params['sender'], 'From' => $this->config['twilio-sender'],
'Body' => $params['text'], 'Body' => $alert_data['msg'],
'To' => $params['phone'], 'To' => $this->config['twilio-to'],
]; ];
$post = http_build_query($data);
$curl = curl_init($url); $res = Http::client()->asForm()
->withBasicAuth($this->config['twilio-sid'], $this->config['twilio-token'])
->post($url, $data);
Proxy::applyToCurl($curl); if ($res->successful()) {
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($curl, CURLOPT_USERPWD, $params['sid'] . ':' . $params['token']);
curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
curl_exec($curl);
if (curl_getinfo($curl, CURLINFO_RESPONSE_CODE)) {
return true; return true;
} }
throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $alert_data['msg'], $data);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -24,18 +24,14 @@
namespace LibreNMS\Alert\Transport; namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Ukfastpss extends Transport class Ukfastpss extends Transport
{ {
protected $name = 'UKFast PSS'; protected string $name = 'UKFast PSS';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{
return $this->contactUkfastpss($obj, $opts);
}
public function contactUkfastpss($obj, $opts)
{ {
$apiKey = $this->config['api-key']; $apiKey = $this->config['api-key'];
$author = $this->config['author']; $author = $this->config['author'];
@ -47,34 +43,27 @@ class Ukfastpss extends Transport
'id' => $author, 'id' => $author,
], ],
'secure' => ($secure == 'on'), 'secure' => ($secure == 'on'),
'subject' => $obj['title'], 'subject' => $alert_data['title'],
'details' => $obj['msg'], 'details' => $alert_data['msg'],
'priority' => $priority, 'priority' => $priority,
]; ];
$request_opts = []; $res = Http::client()
$request_headers = []; ->withHeaders([
'Authorization' => $apiKey,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->post('https://api.ukfast.io/pss/v1/requests', $body);
$request_headers['Authorization'] = $apiKey; if ($res->successful()) {
$request_headers['Content-Type'] = 'application/json'; return true;
$request_headers['Accept'] = 'application/json';
$client = new \GuzzleHttp\Client();
$request_opts['proxy'] = Proxy::forGuzzle();
$request_opts['headers'] = $request_headers;
$request_opts['body'] = json_encode($body);
$res = $client->request('POST', 'https://api.ukfast.io/pss/v1/requests', $request_opts);
$code = $res->getStatusCode();
if ($code != 200) {
return 'HTTP Status code ' . $code;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $alert_data['msg'], $body);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -26,62 +26,47 @@ namespace LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Enum\AlertState; use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Proxy; use LibreNMS\Exceptions\AlertTransportDeliveryException;
use LibreNMS\Util\Http;
class Victorops extends Transport class Victorops extends Transport
{ {
protected $name = 'VictorOps'; protected string $name = 'Splunk On-Call';
public function deliverAlert($obj, $opts) public function deliverAlert(array $alert_data): bool
{ {
if (! empty($this->config)) { $url = $this->config['victorops-url'];
$opts['url'] = $this->config['victorops-url'];
}
return $this->contactVictorops($obj, $opts);
}
public function contactVictorops($obj, $opts)
{
$url = $opts['url'];
$protocol = [ $protocol = [
'entity_id' => strval($obj['id'] ? $obj['id'] : $obj['uid']), 'entity_id' => strval($alert_data['id'] ?: $alert_data['uid']),
'state_start_time' => strtotime($obj['timestamp']), 'state_start_time' => strtotime($alert_data['timestamp']),
'entity_display_name' => $obj['title'], 'entity_display_name' => $alert_data['title'],
'state_message' => $obj['msg'], 'state_message' => $alert_data['msg'],
'monitoring_tool' => 'librenms', 'monitoring_tool' => 'librenms',
]; ];
if ($obj['state'] == AlertState::RECOVERED) { $protocol['message_type'] = match ($alert_data['state']) {
$protocol['message_type'] = 'recovery'; AlertState::RECOVERED => 'RECOVERY',
} elseif ($obj['state'] == AlertState::ACKNOWLEDGED) { AlertState::ACKNOWLEDGED => 'ACKNOWLEDGEMENT',
$protocol['message_type'] = 'acknowledgement'; default => match ($alert_data['severity']) {
} elseif ($obj['state'] == AlertState::ACTIVE) { 'ok' => 'INFO',
$protocol['message_type'] = 'critical'; 'warning' => 'WARNING',
} default => 'CRITICAL',
},
};
foreach ($obj['faults'] as $fault => $data) { foreach ($alert_data['faults'] as $fault => $data) {
$protocol['state_message'] .= $data['string']; $protocol['state_message'] .= $data['string'];
} }
$curl = curl_init(); $res = Http::client()->post($url, $protocol);
Proxy::applyToCurl($curl);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-type' => 'application/json']);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($protocol));
$ret = curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200) {
var_dump('VictorOps returned Error, retry later'); //FIXME: propper debuging
return false; if ($res->successful()) {
return true;
} }
return true; throw new AlertTransportDeliveryException($alert_data, $res->status(), $res->body(), $alert_data['msg'], $protocol);
} }
public static function configTemplate() public static function configTemplate(): array
{ {
return [ return [
'config' => [ 'config' => [

View File

@ -27,40 +27,34 @@
namespace LibreNMS\Data\Store; namespace LibreNMS\Data\Store;
use App\Polling\Measure\Measurement; use App\Polling\Measure\Measurement;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use LibreNMS\Config; use LibreNMS\Config;
use LibreNMS\Util\Proxy; use LibreNMS\Util\Http;
use Log; use Log;
class Prometheus extends BaseDatastore class Prometheus extends BaseDatastore
{ {
private $client; private $client;
private $base_uri; private $base_uri;
private $default_opts;
private $enabled; private $enabled;
private $prefix; private $prefix;
public function __construct(\GuzzleHttp\Client $client) public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->client = $client;
$url = Config::get('prometheus.url'); $url = Config::get('prometheus.url');
$job = Config::get('prometheus.job', 'librenms'); $job = Config::get('prometheus.job', 'librenms');
$this->base_uri = "$url/metrics/job/$job/instance/"; $this->base_uri = "$url/metrics/job/$job/instance/";
$this->client = Http::client()->baseUrl($this->base_uri);
$this->prefix = Config::get('prometheus.prefix', ''); $this->prefix = Config::get('prometheus.prefix', '');
if ($this->prefix) { if ($this->prefix) {
$this->prefix = "$this->prefix" . '_'; $this->prefix = "$this->prefix" . '_';
} }
$this->default_opts = [
'headers' => ['Content-Type' => 'text/plain'],
];
if ($proxy = Proxy::get($url)) {
$this->default_opts['proxy'] = $proxy;
}
$this->enabled = self::isEnabled(); $this->enabled = self::isEnabled();
} }
@ -82,52 +76,41 @@ class Prometheus extends BaseDatastore
return; return;
} }
try { $vals = '';
$vals = ''; $promtags = '/measurement/' . $measurement;
$promtags = '/measurement/' . $measurement;
foreach ($fields as $k => $v) { foreach ($fields as $k => $v) {
if ($v !== null) { if ($v !== null) {
$vals .= $this->prefix . "$k $v\n"; $vals .= $this->prefix . "$k $v\n";
}
} }
foreach ($tags as $t => $v) {
if ($v !== null) {
$promtags .= (Str::contains($v, '/') ? "/$t@base64/" . base64_encode($v) : "/$t/$v");
}
}
$options = $this->getDefaultOptions();
$options['body'] = $vals;
$promurl = $this->base_uri . $device['hostname'] . $promtags;
if (Config::get('prometheus.attach_sysname', false)) {
$promurl .= '/sysName/' . $device['sysName'];
}
$promurl = str_replace(' ', '-', $promurl); // Prometheus doesn't handle tags with spaces in url
Log::debug("Prometheus put $promurl: ", [
'measurement' => $measurement,
'tags' => $tags,
'fields' => $fields,
'vals' => $vals,
]);
$result = $this->client->request('POST', $promurl, $options);
$this->recordStatistic($stat->end());
if ($result->getStatusCode() !== 200) {
Log::error('Prometheus Error: ' . $result->getReasonPhrase());
}
} catch (GuzzleException $e) {
Log::error('Prometheus Exception: ' . $e->getMessage());
} }
}
private function getDefaultOptions() foreach ($tags as $t => $v) {
{ if ($v !== null) {
return $this->default_opts; $promtags .= (Str::contains($v, '/') ? "/$t@base64/" . base64_encode($v) : "/$t/$v");
}
}
$promurl = $device['hostname'] . $promtags;
if (Config::get('prometheus.attach_sysname', false)) {
$promurl .= '/sysName/' . $device['sysName'];
}
$promurl = str_replace(' ', '-', $promurl); // Prometheus doesn't handle tags with spaces in url
Log::debug("Prometheus put $promurl: ", [
'measurement' => $measurement,
'tags' => $tags,
'fields' => $fields,
'vals' => $vals,
]);
$result = $this->client->withBody($vals, 'text/plain')->post($promurl);
$this->recordStatistic($stat->end());
if (! $result->successful()) {
Log::error('Prometheus Error: ' . $result->body());
}
} }
/** /**

View File

@ -27,26 +27,13 @@ namespace LibreNMS\Exceptions;
class AlertTransportDeliveryException extends \Exception class AlertTransportDeliveryException extends \Exception
{ {
/** @var array */ public function __construct(
protected $params = []; array $data,
/** @var string */ int $code = 0,
protected $template = ''; protected string $response = '',
/** @var string */ protected string $template = '',
protected $response = ''; protected array $params = []
) {
/**
* @param array $data
* @param int $code
* @param string $response
* @param string $message
* @param array $params
*/
public function __construct($data, $code = 0, $response = '', $message = '', $params = [])
{
$this->params = $params;
$this->template = $message;
$this->response = $response;
$name = $data['transport_name'] ?? ''; $name = $data['transport_name'] ?? '';
$message = "Transport delivery failed with $code for $name: $response"; $message = "Transport delivery failed with $code for $name: $response";

View File

@ -36,17 +36,16 @@ interface Transport
* Gets called when an alert is sent * Gets called when an alert is sent
* *
* @param array $alert_data An array created by DescribeAlert * @param array $alert_data An array created by DescribeAlert
* @param array|true $opts The options from the alert_transports transport_config column * @return bool Returns true if the call was successful.
* @return mixed Returns if the call was successful
* *
* @throws \LibreNMS\Exceptions\AlertTransportDeliveryException * @throws \LibreNMS\Exceptions\AlertTransportDeliveryException
*/ */
public function deliverAlert($alert_data, $opts); public function deliverAlert(array $alert_data): bool;
/** /**
* @return array * @return array
*/ */
public static function configTemplate(); public static function configTemplate(): array;
/** /**
* Display the configuration details of this alert transport * Display the configuration details of this alert transport

View File

@ -196,7 +196,7 @@ class Git
return $this->cacheGet('remoteCommit', function () { return $this->cacheGet('remoteCommit', function () {
if ($this->isAvailable()) { if ($this->isAvailable()) {
try { try {
return (array) \Http::withOptions(['proxy' => Proxy::forGuzzle()])->get(Config::get('github_api') . 'commits/master')->json(); return (array) Http::client()->get(Config::get('github_api') . 'commits/master')->json();
} catch (ConnectionException $e) { } catch (ConnectionException $e) {
} }
} }

View File

@ -27,7 +27,6 @@
namespace LibreNMS\Util; namespace LibreNMS\Util;
use Exception; use Exception;
use Illuminate\Support\Facades\Http;
class GitHub class GitHub
{ {
@ -112,7 +111,7 @@ class GitHub
*/ */
public function getRelease($tag) public function getRelease($tag)
{ {
$release = Http::withHeaders($this->getHeaders())->get($this->github . "/releases/tags/$tag"); $release = Http::client()->withHeaders($this->getHeaders())->get($this->github . "/releases/tags/$tag");
return $release->json(); return $release->json();
} }
@ -122,7 +121,7 @@ class GitHub
*/ */
public function getPullRequest() public function getPullRequest()
{ {
$pull_request = Http::withHeaders($this->getHeaders())->get($this->github . "/pulls/{$this->pr}"); $pull_request = Http::client()->withHeaders($this->getHeaders())->get($this->github . "/pulls/{$this->pr}");
$this->pr = $pull_request->json(); $this->pr = $pull_request->json();
} }
@ -180,7 +179,7 @@ class GitHub
} }
GRAPHQL; GRAPHQL;
$prs = Http::withHeaders($this->getHeaders())->post($this->graphql, ['query' => $query]); $prs = Http::client()->withHeaders($this->getHeaders())->post($this->graphql, ['query' => $query]);
$prs = $prs->json(); $prs = $prs->json();
if (! isset($prs['data'])) { if (! isset($prs['data'])) {
var_dump($prs); var_dump($prs);
@ -366,7 +365,7 @@ GRAPHQL;
$this->createChangelog(false); $this->createChangelog(false);
} }
$release = Http::withHeaders($this->getHeaders())->post($this->github . '/releases', [ $release = Http::client()->withHeaders($this->getHeaders())->post($this->github . '/releases', [
'tag_name' => $this->tag, 'tag_name' => $this->tag,
'target_commitish' => $updated_sha, 'target_commitish' => $updated_sha,
'body' => $this->markdown, 'body' => $this->markdown,
@ -422,10 +421,10 @@ GRAPHQL;
*/ */
private function pushFileContents($file, $contents, $message): string private function pushFileContents($file, $contents, $message): string
{ {
$existing = Http::withHeaders($this->getHeaders())->get($this->github . '/contents/' . $file); $existing = Http::client()->withHeaders($this->getHeaders())->get($this->github . '/contents/' . $file);
$existing_sha = $existing->json()['sha']; $existing_sha = $existing->json()['sha'];
$updated = Http::withHeaders($this->getHeaders())->put($this->github . '/contents/' . $file, [ $updated = Http::client()->withHeaders($this->getHeaders())->put($this->github . '/contents/' . $file, [
'message' => $message, 'message' => $message,
'content' => base64_encode($contents), 'content' => base64_encode($contents),
'sha' => $existing_sha, 'sha' => $existing_sha,

49
LibreNMS/Util/Http.php Normal file
View File

@ -0,0 +1,49 @@
<?php
/**
* Http.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 <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2022 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Util;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http as LaravelHttp;
use LibreNMS\Config;
class Http
{
/**
* Create a new client with proxy set if appropriate and a distinct User-Agent header
*/
public static function client(): PendingRequest
{
return LaravelHttp::withOptions([
'proxy' => [
'http' => Proxy::http(),
'https' => Proxy::https(),
'no' => Proxy::ignore(),
],
])->withHeaders([
'User-Agent' => Config::get('project_name') . '/' . Version::VERSION, // we don't need fine version here, just rough
]);
}
}

View File

@ -29,67 +29,29 @@ use LibreNMS\Config;
class Proxy class Proxy
{ {
/** public static function http(): string
* Check if if the proxy should be used.
* (it should not be used for connections to localhost)
*/
public static function shouldBeUsed(string $target_url): bool
{ {
return preg_match('#(^|://)(localhost|127\.|::1)#', $target_url) == 0; // use local_only to avoid CVE-2016-5385
$http_proxy = getenv('http_proxy', local_only: true) ?: getenv('HTTP_PROXY', local_only: true) ?: Config::get('http_proxy', '');
return $http_proxy;
} }
/** public static function https(): string
* Return the proxy url
*
* @return array|bool|false|string
*/
public static function get(?string $target_url = null)
{ {
if ($target_url && ! self::shouldBeUsed($target_url)) { // use local_only to avoid CVE-2016-5385
return false; return getenv('https_proxy', local_only: true) ?: getenv('HTTPS_PROXY', local_only: true) ?: Config::get('https_proxy', '');
} elseif (getenv('http_proxy')) { }
return getenv('http_proxy');
} elseif (getenv('https_proxy')) { public static function ignore(): array
return getenv('https_proxy'); {
} elseif ($callback_proxy = Config::get('callback_proxy')) { // use local_only to avoid CVE-2016-5385
return $callback_proxy; $no_proxy = getenv('no_proxy', local_only: true) ?: getenv('NO_PROXY', local_only: true) ?: Config::get('no_proxy', '');
} elseif ($http_proxy = Config::get('http_proxy')) {
return $http_proxy; if ($no_proxy == '') {
return [];
} }
return false; return explode(',', str_replace(' ', '', $no_proxy));
}
/**
* Return the proxy url in guzzle format "http://127.0.0.1:8888"
*/
public static function forGuzzle(?string $target_url = null): string
{
$proxy = self::forCurl($target_url);
return empty($proxy) ? '' : ('http://' . $proxy);
}
/**
* Get the ip and port of the proxy
*
* @return string
*/
public static function forCurl(?string $target_url = null): string
{
return str_replace(['http://', 'https://'], '', rtrim(self::get($target_url), '/'));
}
/**
* Set the proxy on a curl handle
*
* @param \CurlHandle $curl
*/
public static function applyToCurl($curl): void
{
$proxy = self::forCurl();
if (! empty($proxy)) {
curl_setopt($curl, CURLOPT_PROXY, $proxy);
}
} }
} }

View File

@ -38,7 +38,7 @@ class Stats
$stats = new static; $stats = new static;
if ($stats->isEnabled()) { if ($stats->isEnabled()) {
$response = \Http::withOptions(['proxy' => Proxy::forGuzzle()]) Http::client()
->asForm() ->asForm()
->post(\LibreNMS\Config::get('callback_post'), [ ->post(\LibreNMS\Config::get('callback_post'), [
'data' => json_encode($stats->collectData()), 'data' => json_encode($stats->collectData()),
@ -63,7 +63,7 @@ class Stats
{ {
$uuid = Callback::get('uuid'); $uuid = Callback::get('uuid');
$response = \Http::withOptions(['proxy' => Proxy::forGuzzle()]) $response = Http::client()
->asForm() ->asForm()
->post(\LibreNMS\Config::get('callback_clear'), ['uuid' => $uuid]); ->post(\LibreNMS\Config::get('callback_clear'), ['uuid' => $uuid]);

View File

@ -25,8 +25,7 @@
namespace App\ApiClients; namespace App\ApiClients;
use Illuminate\Support\Facades\Http; use LibreNMS\Util\Http;
use LibreNMS\Util\Proxy;
class BaseApi class BaseApi
{ {
@ -37,9 +36,7 @@ class BaseApi
protected function getClient(): \Illuminate\Http\Client\PendingRequest protected function getClient(): \Illuminate\Http\Client\PendingRequest
{ {
if (is_null($this->client)) { if (is_null($this->client)) {
$this->client = Http::withOptions([ $this->client = Http::client()->baseUrl($this->base_uri)
'proxy' => Proxy::forGuzzle($this->base_uri),
])->baseUrl($this->base_uri)
->timeout($this->timeout); ->timeout($this->timeout);
} }

View File

@ -49,7 +49,7 @@ class BingApi extends BaseApi implements Geocoder
} }
/** /**
* Build Guzzle request option array * Build request option array
* *
* @throws \Exception you may throw an Exception if validation fails * @throws \Exception you may throw an Exception if validation fails
*/ */

View File

@ -83,7 +83,7 @@ trait GeocodingHelper
abstract protected function parseLatLng(array $data): array; abstract protected function parseLatLng(array $data): array;
/** /**
* Build Guzzle request option array * Build request option array
* *
* @param string $address * @param string $address
* @return array * @return array

View File

@ -60,7 +60,7 @@ class GoogleMapsApi extends BaseApi implements Geocoder
} }
/** /**
* Build Guzzle request option array * Build request option array
* *
* @throws \Exception you may throw an Exception if validation fails * @throws \Exception you may throw an Exception if validation fails
*/ */

View File

@ -26,12 +26,12 @@
namespace App\ApiClients; namespace App\ApiClients;
use App\Models\Device; use App\Models\Device;
use GuzzleHttp\Client;
use LibreNMS\Config; use LibreNMS\Config;
use LibreNMS\Util\Http;
class GraylogApi class GraylogApi
{ {
private Client $client; private \Illuminate\Http\Client\PendingRequest $client;
private string $api_prefix = ''; private string $api_prefix = '';
public function __construct(array $config = []) public function __construct(array $config = [])
@ -53,7 +53,7 @@ class GraylogApi
]; ];
} }
$this->client = new Client($config); $this->client = Http::client()->withOptions($config);
} }
public function getStreams(): array public function getStreams(): array
@ -65,9 +65,8 @@ class GraylogApi
$uri = $this->api_prefix . '/streams'; $uri = $this->api_prefix . '/streams';
$response = $this->client->get($uri); $response = $this->client->get($uri);
$data = json_decode($response->getBody(), true);
return $data ?: []; return $response->json() ?: [];
} }
/** /**
@ -93,10 +92,9 @@ class GraylogApi
'filter' => $filter, 'filter' => $filter,
]; ];
$response = $this->client->get($uri, ['query' => $data]); $response = $this->client->get($uri, $data);
$data = json_decode($response->getBody(), true);
return $data ?: []; return $response->json() ?: [];
} }
/** /**

View File

@ -49,7 +49,7 @@ class MapquestApi extends BaseApi implements Geocoder
} }
/** /**
* Build Guzzle request option array * Build request option array
* *
* @throws \Exception you may throw an Exception if validation fails * @throws \Exception you may throw an Exception if validation fails
*/ */

View File

@ -46,7 +46,7 @@ class NominatimApi extends BaseApi implements Geocoder
} }
/** /**
* Build Guzzle request option array * Build request option array
* *
* @throws \Exception you may throw an Exception if validation fails * @throws \Exception you may throw an Exception if validation fails
*/ */

View File

@ -25,7 +25,6 @@
namespace App\ApiClients; namespace App\ApiClients;
use GuzzleHttp\Exception\RequestException;
use LibreNMS\Exceptions\ApiClientException; use LibreNMS\Exceptions\ApiClientException;
class RipeApi extends BaseApi class RipeApi extends BaseApi
@ -68,21 +67,12 @@ class RipeApi extends BaseApi
*/ */
private function makeApiCall(string $uri, array $options): mixed private function makeApiCall(string $uri, array $options): mixed
{ {
try { $response_data = $this->getClient()->get($uri, $options['query'])->json();
$response_data = $this->getClient()->get($uri, $options['query'])->json();
if (isset($response_data['status']) && $response_data['status'] == 'ok') {
return $response_data;
} else {
throw new ApiClientException('RIPE API call failed', $response_data);
}
} catch (RequestException $e) {
$message = 'RIPE API call to ' . $e->getRequest()->getUri() . ' failed: ';
$message .= $e->getResponse()->getReasonPhrase() . ' ' . $e->getResponse()->getStatusCode();
throw new ApiClientException( if (isset($response_data['status']) && $response_data['status'] == 'ok') {
$message, return $response_data;
json_decode($e->getResponse()->getBody(), true)
);
} }
throw new ApiClientException("RIPE API call to $this->base_uri/$uri failed: " . $this->getClient()->get($uri, $options['query'])->status(), $response_data);
} }
} }

View File

@ -54,6 +54,7 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use LibreNMS\Config; use LibreNMS\Config;
use LibreNMS\Data\Store\Rrd; use LibreNMS\Data\Store\Rrd;
use LibreNMS\Util\Http;
use LibreNMS\Util\Version; use LibreNMS\Util\Version;
class AboutController extends Controller class AboutController extends Controller
@ -113,7 +114,7 @@ class AboutController extends Controller
// try to clear usage data if we have a uuid // try to clear usage data if we have a uuid
if ($usage_uuid) { if ($usage_uuid) {
if (! \Http::post(Config::get('callback_clear'), ['uuid' => $usage_uuid])->successful()) { if (! Http::client()->post(Config::get('callback_clear'), ['uuid' => $usage_uuid])->successful()) {
return response()->json([], 500); // don't clear if this fails to delete upstream data return response()->json([], 500); // don't clear if this fails to delete upstream data
} }
} }

View File

@ -5,41 +5,19 @@ namespace App\Http\Controllers;
use App\Models\AlertTransport; use App\Models\AlertTransport;
use App\Models\Device; use App\Models\Device;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use LibreNMS\Alert\AlertUtil; use LibreNMS\Alert\AlertData;
use LibreNMS\Config;
use LibreNMS\Exceptions\AlertTransportDeliveryException; use LibreNMS\Exceptions\AlertTransportDeliveryException;
class AlertTransportController extends Controller class AlertTransportController extends Controller
{ {
public function test(Request $request, AlertTransport $transport): \Illuminate\Http\JsonResponse public function test(Request $request, AlertTransport $transport): \Illuminate\Http\JsonResponse
{ {
/** @var Device $device */
$device = Device::with('location')->first(); $device = Device::with('location')->first();
$obj = [ $alert_data = AlertData::testData($device);
'hostname' => $device->hostname,
'device_id' => $device->device_id,
'sysDescr' => $device->sysDescr,
'version' => $device->version,
'hardware' => $device->hardware,
'location' => $device->location,
'title' => 'Testing transport from ' . Config::get('project_name'),
'elapsed' => '11s',
'alert_id' => '000',
'id' => '000',
'faults' => [],
'uid' => '000',
'severity' => 'critical',
'rule' => 'macros.device = 1',
'name' => 'Test-Rule',
'string' => '#1: test => string;',
'timestamp' => date('Y-m-d H:i:s'),
'contacts' => AlertUtil::getContacts($device->toArray()),
'state' => '1',
'msg' => 'This is a test alert',
];
$opts = Config::get('alert.transports.' . $transport->transport_type);
try { try {
$result = $transport->instance()->deliverAlert($obj, $opts); $result = $transport->instance()->deliverAlert($alert_data);
if ($result === true) { if ($result === true) {
return response()->json(['status' => 'ok']); return response()->json(['status' => 'ok']);
@ -47,7 +25,7 @@ class AlertTransportController extends Controller
} catch (AlertTransportDeliveryException $e) { } catch (AlertTransportDeliveryException $e) {
return response()->json([ return response()->json([
'status' => 'error', 'status' => 'error',
'message' => $e->getMessage(), 'message' => strip_tags($e->getMessage()),
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error($e); \Log::error($e);
@ -56,7 +34,7 @@ class AlertTransportController extends Controller
return response()->json([ return response()->json([
'status' => 'error', 'status' => 'error',
'message' => $result, 'message' => strip_tags($result),
]); ]);
} }
} }

View File

@ -5,27 +5,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use LibreNMS\Alert\Transport; use LibreNMS\Alert\Transport;
use LibreNMS\Alert\Transport\Dummy;
/**
* \App\Models\AlertTransport
*
* @property int $transport_id
* @property string $transport_name
* @property string $transport_type
* @property bool $is_default
* @property array|null $transport_config
*
* @method static \Illuminate\Database\Eloquent\Builder|AlertTransport newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|AlertTransport newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|AlertTransport query()
* @method static \Illuminate\Database\Eloquent\Builder|AlertTransport whereIsDefault($value)
* @method static \Illuminate\Database\Eloquent\Builder|AlertTransport whereTransportConfig($value)
* @method static \Illuminate\Database\Eloquent\Builder|AlertTransport whereTransportId($value)
* @method static \Illuminate\Database\Eloquent\Builder|AlertTransport whereTransportName($value)
* @method static \Illuminate\Database\Eloquent\Builder|AlertTransport whereTransportType($value)
*
* @mixin \Eloquent
*/
class AlertTransport extends Model class AlertTransport extends Model
{ {
use HasFactory; use HasFactory;
@ -36,11 +17,16 @@ class AlertTransport extends Model
'is_default' => 'boolean', 'is_default' => 'boolean',
'transport_config' => 'array', 'transport_config' => 'array',
]; ];
protected $fillable = ['transport_config'];
public function instance(): Transport public function instance(): Transport
{ {
$class = Transport::getClass($this->transport_type); $class = Transport::getClass($this->transport_type);
return new $class($this); if (class_exists($class)) {
return new $class($this);
}
return new Dummy;
} }
} }

View File

@ -32,7 +32,6 @@
"easybook/geshi": "^1.0.8", "easybook/geshi": "^1.0.8",
"ezyang/htmlpurifier": "^4.8", "ezyang/htmlpurifier": "^4.8",
"fico7489/laravel-pivot": "^3.0", "fico7489/laravel-pivot": "^3.0",
"guzzlehttp/guzzle": "^7.2",
"influxdb/influxdb-php": "^1.15", "influxdb/influxdb-php": "^1.15",
"justinrainbow/json-schema": "^5.2", "justinrainbow/json-schema": "^5.2",
"laravel-notification-channels/webpush": "^7.0", "laravel-notification-channels/webpush": "^7.0",

2
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "9921ea55b62f77f05208738b5b783b31", "content-hash": "4a8a1c7d52ce1cb29b37826a53d89804",
"packages": [ "packages": [
{ {
"name": "amenadiel/jpgraph", "name": "amenadiel/jpgraph",

View File

@ -233,6 +233,8 @@ return [
'PluginManager' => \App\Facades\PluginManager::class, 'PluginManager' => \App\Facades\PluginManager::class,
'Rrd' => \App\Facades\Rrd::class, 'Rrd' => \App\Facades\Rrd::class,
'SnmpQuery' => \App\Facades\FacadeAccessorSnmp::class, 'SnmpQuery' => \App\Facades\FacadeAccessorSnmp::class,
])->forget([
'Http', // don't use Laravel Http facade, LibreNMS has its own wrapper
])->toArray(), ])->toArray(),
'charset' => env('CHARSET', ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8'), 'charset' => env('CHARSET', ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8'),

View File

@ -183,19 +183,6 @@ You need a token you can find on your personnal space.
| API URL | https://soap.aspsms.com/aspsmsx.asmx/SimpleTextSMS | | API URL | https://soap.aspsms.com/aspsmsx.asmx/SimpleTextSMS |
| Options | UserKey=USERKEY<br />Password=APIPASSWORD<br />Recipient=RECIPIENT<br/> Originator=ORIGINATOR<br />MessageText={{ $msg }} | | Options | UserKey=USERKEY<br />Password=APIPASSWORD<br />Recipient=RECIPIENT<br/> Originator=ORIGINATOR<br />MessageText={{ $msg }} |
## Boxcar
Copy your access token from the Boxcar app or from the Boxcar.io
website and setup the transport.
[Boxcar Docs](http://developer.boxcar.io/api/publisher/)
**Example:**
| Config | Example |
| ------ | ------- |
| Access Token | i23f23mr23rwerw |
## Browser Push ## Browser Push
Browser push notifications can send a notification to the user's Browser push notifications can send a notification to the user's
@ -322,9 +309,9 @@ for details on acceptable values.
| API URL | <https://api.hipchat.com/v1/rooms/message?auth_token=109jawregoaihj> | | API URL | <https://api.hipchat.com/v1/rooms/message?auth_token=109jawregoaihj> |
| Room ID | 7654321 | | Room ID | 7654321 |
| From Name | LibreNMS | | From Name | LibreNMS |
| Options | color = red <br/> notify = 1 <br/> message_format = text | | Options | color=red |
At present the following options are supported: `color`, `notify` and `message_format`. At present the following options are supported: `color`.
> Note: The default message format for HipChat messages is HTML. It is > Note: The default message format for HipChat messages is HTML. It is
> recommended that you specify the `text` message format to prevent unexpected > recommended that you specify the `text` message format to prevent unexpected
@ -536,7 +523,7 @@ Here an example using 3 numbers, any amount of numbers is supported:
| Config | Example | | Config | Example |
| ------ | ------- | | ------ | ------- |
| PlaySMS | <https://localhost/index.php?app=ws> | | PlaySMS | <https://localhost/index.php> |
| User | user1 | | User | user1 |
| Token | MYFANCYACCESSTOKEN | | Token | MYFANCYACCESSTOKEN |
| From | My Name | | From | My Name |

View File

@ -9,7 +9,6 @@
*/ */
use App\Models\Device; use App\Models\Device;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use LibreNMS\Config; use LibreNMS\Config;
use LibreNMS\Enum\PortAssociationMode; use LibreNMS\Enum\PortAssociationMode;
@ -23,7 +22,6 @@ use LibreNMS\Exceptions\HostUnreachableSnmpException;
use LibreNMS\Exceptions\InvalidPortAssocModeException; use LibreNMS\Exceptions\InvalidPortAssocModeException;
use LibreNMS\Exceptions\SnmpVersionUnsupportedException; use LibreNMS\Exceptions\SnmpVersionUnsupportedException;
use LibreNMS\Modules\Core; use LibreNMS\Modules\Core;
use LibreNMS\Util\Proxy;
function array_sort_by_column($array, $on, $order = SORT_ASC) function array_sort_by_column($array, $on, $order = SORT_ASC)
{ {
@ -759,14 +757,6 @@ function guidv4($data)
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
} }
/**
* @param $curl
*/
function set_curl_proxy($curl)
{
\LibreNMS\Util\Proxy::applyToCurl($curl);
}
function target_to_id($target) function target_to_id($target)
{ {
if ($target[0] . $target[1] == 'g:') { if ($target[0] . $target[1] == 'g:') {
@ -1197,7 +1187,7 @@ function cache_mac_oui()
//$mac_oui_url_mirror = 'https://raw.githubusercontent.com/wireshark/wireshark/master/manuf'; //$mac_oui_url_mirror = 'https://raw.githubusercontent.com/wireshark/wireshark/master/manuf';
echo ' -> Downloading ...' . PHP_EOL; echo ' -> Downloading ...' . PHP_EOL;
$get = Http::withOptions(['proxy' => Proxy::forGuzzle()])->get($mac_oui_url); $get = \LibreNMS\Util\Http::client()->get($mac_oui_url);
echo ' -> Processing CSV ...' . PHP_EOL; echo ' -> Processing CSV ...' . PHP_EOL;
$csv_data = $get->body(); $csv_data = $get->body();
foreach (explode("\n", $csv_data) as $csv_line) { foreach (explode("\n", $csv_data) as $csv_line) {
@ -1255,7 +1245,7 @@ function cache_peeringdb()
$ix_keep = []; $ix_keep = [];
foreach (dbFetchRows('SELECT `bgpLocalAs` FROM `devices` WHERE `disabled` = 0 AND `ignore` = 0 AND `bgpLocalAs` > 0 AND (`bgpLocalAs` < 64512 OR `bgpLocalAs` > 65535) AND `bgpLocalAs` < 4200000000 GROUP BY `bgpLocalAs`') as $as) { foreach (dbFetchRows('SELECT `bgpLocalAs` FROM `devices` WHERE `disabled` = 0 AND `ignore` = 0 AND `bgpLocalAs` > 0 AND (`bgpLocalAs` < 64512 OR `bgpLocalAs` > 65535) AND `bgpLocalAs` < 4200000000 GROUP BY `bgpLocalAs`') as $as) {
$asn = $as['bgpLocalAs']; $asn = $as['bgpLocalAs'];
$get = Http::withOptions(['proxy' => Proxy::forGuzzle()])->get($peeringdb_url . '/net?depth=2&asn=' . $asn); $get = \LibreNMS\Util\Http::client()->get($peeringdb_url . '/net?depth=2&asn=' . $asn);
$json_data = $get->body(); $json_data = $get->body();
$data = json_decode($json_data); $data = json_decode($json_data);
$ixs = $data->{'data'}[0]->{'netixlan_set'}; $ixs = $data->{'data'}[0]->{'netixlan_set'};
@ -1276,7 +1266,7 @@ function cache_peeringdb()
$pdb_ix_id = dbInsert($insert, 'pdb_ix'); $pdb_ix_id = dbInsert($insert, 'pdb_ix');
} }
$ix_keep[] = $pdb_ix_id; $ix_keep[] = $pdb_ix_id;
$get_ix = Http::withOptions(['proxy' => Proxy::forGuzzle()])->get("$peeringdb_url/netixlan?ix_id=$ixid"); $get_ix = \LibreNMS\Util\Http::client()->get("$peeringdb_url/netixlan?ix_id=$ixid");
$ix_json = $get_ix->body(); $ix_json = $get_ix->body();
$ix_data = json_decode($ix_json); $ix_data = json_decode($ix_json);
$peers = $ix_data->{'data'}; $peers = $ix_data->{'data'};

View File

@ -823,8 +823,12 @@ return [
'help' => 'Can be a ENV or HTTP-header field like REMOTE_USER, PHP_AUTH_USER or a custom variant', 'help' => 'Can be a ENV or HTTP-header field like REMOTE_USER, PHP_AUTH_USER or a custom variant',
], ],
'http_proxy' => [ 'http_proxy' => [
'description' => 'HTTP(S) Proxy', 'description' => 'HTTP Proxy',
'help' => 'Set this as a fallback if http_proxy or https_proxy environment variable is not available.', 'help' => 'Set this as a fallback if http_proxy environment variable is not available.',
],
'https_proxy' => [
'description' => 'HTTPS Proxy',
'help' => 'Set this as a fallback if https_proxy environment variable is not available.',
], ],
'ignore_mount' => [ 'ignore_mount' => [
'description' => 'Mountpoints to be ignored', 'description' => 'Mountpoints to be ignored',
@ -968,6 +972,10 @@ return [
'nmap' => [ 'nmap' => [
'description' => 'Path to nmap', 'description' => 'Path to nmap',
], ],
'no_proxy' => [
'description' => 'Proxy Exceptions',
'help' => 'Set this as a fallback if no_proxy environment variable is not available. Comma seperated list of IPs, hosts or domains to ignore.',
],
'opentsdb' => [ 'opentsdb' => [
'enable' => [ 'enable' => [
'description' => 'Enable', 'description' => 'Enable',

View File

@ -3757,7 +3757,25 @@
"group": "system", "group": "system",
"section": "proxy", "section": "proxy",
"order": 0, "order": 0,
"type": "text" "type": "text",
"validate": {
"value": [
"nullable",
"starts_with:http://,https://,tcp://"
]
}
},
"https_proxy": {
"group": "system",
"section": "proxy",
"order": 1,
"type": "text",
"validate": {
"value": [
"nullable",
"starts_with:http://,https://,tcp://"
]
}
}, },
"icmp_check": { "icmp_check": {
"default": true, "default": true,
@ -4352,6 +4370,13 @@
"order": 5, "order": 5,
"type": "executable" "type": "executable"
}, },
"no_proxy": {
"group": "system",
"default": "localhost,127.0.0.1,::1",
"section": "proxy",
"order": 2,
"type": "text"
},
"notifications.LibreNMS": { "notifications.LibreNMS": {
"default": "https://www.librenms.org/notifications.rss", "default": "https://www.librenms.org/notifications.rss",
"type": "text" "type": "text"

View File

@ -1,10 +1,5 @@
parameters: parameters:
ignoreErrors: ignoreErrors:
-
message: "#^Static property LibreNMS\\\\Alert\\\\Transport\\\\Sensu\\:\\:\\$severity is never read, only written\\.$#"
count: 1
path: LibreNMS/Alert/Transport/Sensu.php
- -
message: "#^If condition is always false\\.$#" message: "#^If condition is always false\\.$#"
count: 1 count: 1

View File

@ -27,12 +27,11 @@ namespace LibreNMS\Tests;
use RecursiveDirectoryIterator; use RecursiveDirectoryIterator;
use RecursiveIteratorIterator; use RecursiveIteratorIterator;
use RecursiveRegexIterator;
use RegexIterator; use RegexIterator;
class AlertingTest extends TestCase class AlertingTest extends TestCase
{ {
public function testJsonAlertCollection() public function testJsonAlertCollection(): void
{ {
$rules = get_rules_from_json(); $rules = get_rules_from_json();
$this->assertIsArray($rules); $this->assertIsArray($rules);
@ -41,7 +40,7 @@ class AlertingTest extends TestCase
} }
} }
public function testTransports() public function testTransports(): void
{ {
foreach ($this->getTransportFiles() as $file => $_unused) { foreach ($this->getTransportFiles() as $file => $_unused) {
$parts = explode('/', $file); $parts = explode('/', $file);
@ -52,10 +51,10 @@ class AlertingTest extends TestCase
} }
} }
private function getTransportFiles() private function getTransportFiles(): RegexIterator
{ {
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('LibreNMS/Alert/Transport')); $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('LibreNMS/Alert/Transport'));
return new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); return new RegexIterator($iterator, '/^.+\.php$/i', RegexIterator::GET_MATCH);
} }
} }

View File

@ -22,21 +22,70 @@
namespace LibreNMS\Tests; namespace LibreNMS\Tests;
use LibreNMS\Util\Proxy; use LibreNMS\Config;
use LibreNMS\Util\Http;
use LibreNMS\Util\Version;
class ProxyTest extends TestCase class ProxyTest extends TestCase
{ {
public function testShouldBeUsed(): void public function testClientAgentIsCorrect(): void
{ {
$this->assertTrue(Proxy::shouldBeUsed('http://example.com/foobar')); $this->assertEquals('LibreNMS/' . Version::VERSION, Http::client()->getOptions()['headers']['User-Agent']);
$this->assertTrue(Proxy::shouldBeUsed('foo/bar')); }
$this->assertTrue(Proxy::shouldBeUsed('192.168.0.1'));
$this->assertTrue(Proxy::shouldBeUsed('2001:db8::8a2e:370:7334'));
$this->assertFalse(Proxy::shouldBeUsed('http://localhost/foobar')); public function testProxyIsNotSet(): void
$this->assertFalse(Proxy::shouldBeUsed('localhost/foobar')); {
$this->assertFalse(Proxy::shouldBeUsed('127.0.0.1')); Config::set('http_proxy', '');
$this->assertFalse(Proxy::shouldBeUsed('127.0.0.1:1337')); Config::set('https_proxy', '');
$this->assertFalse(Proxy::shouldBeUsed('::1')); Config::set('no_proxy', '');
$client_options = Http::client()->getOptions();
$this->assertEmpty($client_options['proxy']['http']);
$this->assertEmpty($client_options['proxy']['https']);
$this->assertEmpty($client_options['proxy']['no']);
}
public function testProxyIsSet(): void
{
Config::set('http_proxy', 'http://proxy:5000');
Config::set('https_proxy', 'tcp://proxy:5183');
Config::set('no_proxy', 'localhost,127.0.0.1,::1,.domain.com');
$client_options = Http::client()->getOptions();
$this->assertEquals('http://proxy:5000', $client_options['proxy']['http']);
$this->assertEquals('tcp://proxy:5183', $client_options['proxy']['https']);
$this->assertEquals([
'localhost',
'127.0.0.1',
'::1',
'.domain.com',
], $client_options['proxy']['no']);
}
public function testProxyIsSetFromEnv(): void
{
Config::set('http_proxy', '');
Config::set('https_proxy', '');
Config::set('no_proxy', '');
putenv('HTTP_PROXY=someproxy:3182');
putenv('HTTPS_PROXY=https://someproxy:3182');
putenv('NO_PROXY=.there.com');
$client_options = Http::client()->getOptions();
$this->assertEquals('someproxy:3182', $client_options['proxy']['http']);
$this->assertEquals('https://someproxy:3182', $client_options['proxy']['https']);
$this->assertEquals([
'.there.com',
], $client_options['proxy']['no']);
putenv('http_proxy=otherproxy:3182');
putenv('https_proxy=otherproxy:3183');
putenv('no_proxy=dontproxymebro');
$client_options = Http::client()->getOptions();
$this->assertEquals('otherproxy:3182', $client_options['proxy']['http']);
$this->assertEquals('otherproxy:3183', $client_options['proxy']['https']);
$this->assertEquals([
'dontproxymebro',
], $client_options['proxy']['no']);
} }
} }

View File

@ -1,80 +0,0 @@
<?php
namespace LibreNMS\Tests\Traits;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
/**
* @mixin \LibreNMS\Tests\TestCase
*/
trait MockGuzzleClient
{
/**
* @var MockHandler
*/
private $guzzleMockHandler;
/**
* @var array
*/
private $guzzleConfig;
/**
* @var array
*/
private $guzzleHistory = [];
/**
* Create a Guzzle MockHandler and bind Client with the handler to the Laravel container
*
* @param array $queue Sequential Responses to give to the client.
* @param array $config Guzzle config settings.
*/
public function mockGuzzleClient(array $queue, array $config = []): MockHandler
{
$this->guzzleConfig = $config;
$this->guzzleMockHandler = new MockHandler($queue);
$this->app->bind(Client::class, function () {
$handlerStack = HandlerStack::create($this->guzzleMockHandler);
$handlerStack->push(Middleware::history($this->guzzleHistory));
return new Client(array_merge($this->guzzleConfig, ['handler' => $handlerStack]));
});
return $this->guzzleMockHandler;
}
/**
* Get the request and response history to inspect
*
* @return array
*/
public function guzzleHistory(): array
{
return $this->guzzleHistory;
}
/**
* Get the request history to inspect
*
* @return \GuzzleHttp\Psr7\Request[]
*/
public function guzzleRequestHistory(): array
{
return array_column($this->guzzleHistory, 'request');
}
/**
* Get the response history to inspect
*
* @return \GuzzleHttp\Psr7\Response[]
*/
public function guzzleResponseHistory(): array
{
return array_column($this->guzzleHistory, 'response');
}
}

View File

@ -0,0 +1,155 @@
<?php
/**
* SlackTest.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 <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2022 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Tests\Unit\Alert\Transports;
use App\Models\AlertTransport;
use App\Models\Device;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use LibreNMS\Alert\AlertData;
use LibreNMS\Alert\Transport;
use LibreNMS\Tests\TestCase;
class SlackTest extends TestCase
{
public function testSlackNoConfigDelivery(): void
{
Http::fake();
$slack = new Transport\Slack(new AlertTransport);
/** @var Device $mock_device */
$mock_device = Device::factory()->make();
$slack->deliverAlert(AlertData::testData($mock_device));
Http::assertSent(function (Request $request) {
return
$request->url() == '' &&
$request->method() == 'POST' &&
$request->hasHeader('Content-Type', 'application/json') &&
$request->data() == [
'attachments' => [
[
'fallback' => 'This is a test alert',
'color' => '#ff0000',
'title' => 'Testing transport from LibreNMS',
'text' => 'This is a test alert',
'mrkdwn_in' => [
'text',
'fallback',
],
'author_name' => null,
],
],
'channel' => null,
'icon_emoji' => null,
];
});
}
public function testSlackLegacyDelivery(): void
{
Http::fake();
$slack = new Transport\Slack(new AlertTransport([
'transport_config' => [
'slack-url' => 'https://slack.com/some/webhook',
'slack-options' => "icon_emoji=smile\nauthor=Me\nchannel=Alerts",
],
]));
/** @var Device $mock_device */
$mock_device = Device::factory()->make();
$slack->deliverAlert(AlertData::testData($mock_device));
Http::assertSent(function (Request $request) {
return
$request->url() == 'https://slack.com/some/webhook' &&
$request->method() == 'POST' &&
$request->hasHeader('Content-Type', 'application/json') &&
$request->data() == [
'attachments' => [
[
'fallback' => 'This is a test alert',
'color' => '#ff0000',
'title' => 'Testing transport from LibreNMS',
'text' => 'This is a test alert',
'mrkdwn_in' => [
'text',
'fallback',
],
'author_name' => 'Me',
],
],
'channel' => 'Alerts',
'icon_emoji' => ':smile:',
];
});
}
public function testSlackDelivery(): void
{
Http::fake();
$slack = new Transport\Slack(new AlertTransport([
'transport_config' => [
'slack-url' => 'https://slack.com/some/webhook',
'slack-options' => "icon_emoji=smile\nauthor=Me\nchannel=Alerts",
'slack-icon_emoji' => ':slight_smile:',
'slack-author' => 'Other',
'slack-channel' => 'Critical',
],
]));
/** @var Device $mock_device */
$mock_device = Device::factory()->make();
$slack->deliverAlert(AlertData::testData($mock_device));
Http::assertSent(function (Request $request) {
return
$request->url() == 'https://slack.com/some/webhook' &&
$request->method() == 'POST' &&
$request->hasHeader('Content-Type', 'application/json') &&
$request->data() == [
'attachments' => [
[
'fallback' => 'This is a test alert',
'color' => '#ff0000',
'title' => 'Testing transport from LibreNMS',
'text' => 'This is a test alert',
'mrkdwn_in' => [
'text',
'fallback',
],
'author_name' => 'Other',
],
],
'channel' => 'Critical',
'icon_emoji' => ':slight_smile:',
];
});
}
}

View File

@ -3,34 +3,31 @@
namespace LibreNMS\Tests\Unit; namespace LibreNMS\Tests\Unit;
use App\Models\AlertTransport; use App\Models\AlertTransport;
use GuzzleHttp\Psr7\Response; use Illuminate\Http\Client\Request;
use LibreNMS\Config; use Illuminate\Support\Facades\Http as LaravelHttp;
use LibreNMS\Tests\TestCase; use LibreNMS\Tests\TestCase;
use LibreNMS\Tests\Traits\MockGuzzleClient;
class ApiTransportTest extends TestCase class ApiTransportTest extends TestCase
{ {
use MockGuzzleClient;
public function testGetMultilineVariables(): void public function testGetMultilineVariables(): void
{ {
/** @var AlertTransport $transport */ /** @var AlertTransport $transport */
$transport = AlertTransport::factory()->api('text={{ $msg }}')->make(); $transport = AlertTransport::factory()->api('text={{ $msg }}')->make();
$this->mockGuzzleClient([ LaravelHttp::fake([
new Response(200), '*' => LaravelHttp::response(),
]); ]);
$obj = ['msg' => "This is a multi-line\nalert."]; $obj = ['msg' => "This is a multi-line\nalert."];
$opts = Config::get('alert.transports.' . $transport->transport_type); $result = $transport->instance()->deliverAlert($obj);
$result = $transport->instance()->deliverAlert($obj, $opts);
$this->assertTrue($result); $this->assertTrue($result);
$history = $this->guzzleRequestHistory(); LaravelHttp::assertSentCount(1);
$this->assertCount(1, $history); LaravelHttp::assertSent(function (Request $request) {
$this->assertEquals('GET', $history[0]->getMethod()); return $request->method() == 'GET' &&
$this->assertEquals('text=This%20is%20a%20multi-line%0Aalert.', $history[0]->getUri()->getQuery()); $request->url() == 'https://librenms.org?text=This%20is%20a%20multi-line%0Aalert.';
});
} }
public function testPostMultilineVariables(): void public function testPostMultilineVariables(): void
@ -42,21 +39,20 @@ class ApiTransportTest extends TestCase
'bodytext={{ $msg }}', 'bodytext={{ $msg }}',
)->make(); )->make();
$this->mockGuzzleClient([ LaravelHttp::fake([
new Response(200), '*' => LaravelHttp::response(),
]); ]);
$obj = ['msg' => "This is a post multi-line\nalert."]; $obj = ['msg' => "This is a post multi-line\nalert."];
$opts = Config::get('alert.transports.' . $transport->transport_type); $result = $transport->instance()->deliverAlert($obj);
$result = $transport->instance()->deliverAlert($obj, $opts);
$this->assertTrue($result); $this->assertTrue($result);
$history = $this->guzzleRequestHistory(); LaravelHttp::assertSentCount(1);
$this->assertCount(1, $history); LaravelHttp::assertSent(function (Request $request) {
$this->assertEquals('POST', $history[0]->getMethod()); return $request->method() == 'POST' &&
// FUBAR $request->url() == 'https://librenms.org?text=This%20is%20a%20post%20multi-line%0Aalert.' &&
$this->assertEquals('text=This%20is%20a%20post%20multi-line%0Aalert.', $history[0]->getUri()->getQuery()); $request->body() == "bodytext=This is a post multi-line\nalert.";
$this->assertEquals("bodytext=This is a post multi-line\nalert.", (string) $history[0]->getBody()); });
} }
} }

View File

@ -25,21 +25,16 @@
namespace LibreNMS\Tests\Unit\Data; namespace LibreNMS\Tests\Unit\Data;
use GuzzleHttp\Exception\RequestException; use Illuminate\Support\Facades\Http as LaravelHttp;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use LibreNMS\Config; use LibreNMS\Config;
use LibreNMS\Data\Store\Prometheus; use LibreNMS\Data\Store\Prometheus;
use LibreNMS\Tests\TestCase; use LibreNMS\Tests\TestCase;
use LibreNMS\Tests\Traits\MockGuzzleClient;
/** /**
* @group datastores * @group datastores
*/ */
class PrometheusStoreTest extends TestCase class PrometheusStoreTest extends TestCase
{ {
use MockGuzzleClient;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
@ -48,26 +43,20 @@ class PrometheusStoreTest extends TestCase
Config::set('prometheus.url', 'http://fake:9999'); Config::set('prometheus.url', 'http://fake:9999');
} }
public function testFailWrite() public function testFailWrite(): void
{ {
$this->mockGuzzleClient([ LaravelHttp::fakeSequence()->push('Bad response', 422);
new Response(422, [], 'Bad response'),
new RequestException('Exception thrown', new Request('POST', 'test')),
]);
$prometheus = app(Prometheus::class); $prometheus = app(Prometheus::class);
\Log::shouldReceive('debug'); \Log::shouldReceive('debug');
\Log::shouldReceive('error')->once()->with("Prometheus Exception: Client error: `POST http://fake:9999/metrics/job/librenms/instance/test/measurement/none` resulted in a `422 Unprocessable Entity` response:\nBad response\n"); \Log::shouldReceive('error')->once()->with('Prometheus Error: Bad response');
\Log::shouldReceive('error')->once()->with('Prometheus Exception: Exception thrown');
$prometheus->put(['hostname' => 'test'], 'none', [], ['one' => 1]);
$prometheus->put(['hostname' => 'test'], 'none', [], ['one' => 1]); $prometheus->put(['hostname' => 'test'], 'none', [], ['one' => 1]);
} }
public function testSimpleWrite() public function testSimpleWrite(): void
{ {
$this->mockGuzzleClient([ LaravelHttp::fake([
new Response(200), '*' => LaravelHttp::response(),
]); ]);
$prometheus = app(Prometheus::class); $prometheus = app(Prometheus::class);
@ -82,12 +71,11 @@ class PrometheusStoreTest extends TestCase
$prometheus->put($device, $measurement, $tags, $fields); $prometheus->put($device, $measurement, $tags, $fields);
$history = $this->guzzleRequestHistory(); LaravelHttp::assertSentCount(1);
$this->assertCount(1, $history, 'Did not receive the expected number of requests'); LaravelHttp::assertSent(function (\Illuminate\Http\Client\Request $request) {
$this->assertEquals('POST', $history[0]->getMethod()); return $request->method() == 'POST' &&
$this->assertEquals('/metrics/job/librenms/instance/testhost/measurement/testmeasure/ifName/testifname/type/testtype', $history[0]->getUri()->getPath()); $request->url() == 'http://fake:9999/metrics/job/librenms/instance/testhost/measurement/testmeasure/ifName/testifname/type/testtype' &&
$this->assertEquals('fake', $history[0]->getUri()->getHost()); $request->body() == "ifIn 234234\nifOut 53453\n";
$this->assertEquals(9999, $history[0]->getUri()->getPort()); });
$this->assertEquals("ifIn 234234\nifOut 53453\n", (string) $history[0]->getBody());
} }
} }