mirror of
https://github.com/librenms/librenms.git
synced 2024-10-07 16:52:45 +00:00
Fix api transport mult-line parsing (#13469)
* API transport fix parsing parse templates after parsing user options, not before * API transport tests * fix style and lint * remove accidental item * fix more type issues
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
namespace LibreNMS\Alert;
|
namespace LibreNMS\Alert;
|
||||||
|
|
||||||
use App\Models\AlertTransport;
|
use App\Models\AlertTransport;
|
||||||
|
use App\View\SimpleTemplate;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use LibreNMS\Config;
|
use LibreNMS\Config;
|
||||||
@@ -24,6 +25,24 @@ abstract class Transport implements TransportInterface
|
|||||||
return new $class();
|
return new $class();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all available transports
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function list(): array
|
||||||
|
{
|
||||||
|
$list = [];
|
||||||
|
foreach (glob(base_path('LibreNMS/Alert/Transport/*.php')) as $file) {
|
||||||
|
$transport = strtolower(basename($file, '.php'));
|
||||||
|
$class = self::getClass($transport);
|
||||||
|
$instance = new $class;
|
||||||
|
$list[$transport] = $instance->name();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transport constructor.
|
* Transport constructor.
|
||||||
*
|
*
|
||||||
@@ -63,15 +82,16 @@ abstract class Transport implements TransportInterface
|
|||||||
* Helper function to parse free form text box defined in ini style to key value pairs
|
* Helper function to parse free form text box defined in ini style to key value pairs
|
||||||
*
|
*
|
||||||
* @param string $input
|
* @param string $input
|
||||||
|
* @param array $replacements for SimpleTemplate if desired
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function parseUserOptions(string $input): array
|
protected function parseUserOptions(string $input, array $replacements = []): array
|
||||||
{
|
{
|
||||||
$options = [];
|
$options = [];
|
||||||
foreach (preg_split('/\\r\\n|\\r|\\n/', $input, -1, PREG_SPLIT_NO_EMPTY) as $option) {
|
foreach (preg_split('/\\r\\n|\\r|\\n/', $input, -1, PREG_SPLIT_NO_EMPTY) as $option) {
|
||||||
if (Str::contains($option, '=')) {
|
if (Str::contains($option, '=')) {
|
||||||
[$k, $v] = explode('=', $option, 2);
|
[$k, $v] = explode('=', $option, 2);
|
||||||
$options[$k] = trim($v);
|
$options[$k] = empty($replacements) ? trim($v) : SimpleTemplate::parse(trim($v), $replacements);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -52,11 +52,11 @@ class Api extends Transport
|
|||||||
$host = explode('?', $api, 2)[0]; //we don't use the parameter part, cause we build it out of options.
|
$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;
|
//get each line of key-values and process the variables for Headers;
|
||||||
$request_heads = $this->parseUserOptions(SimpleTemplate::parse($headers, $obj));
|
$request_heads = $this->parseUserOptions($headers, $obj);
|
||||||
//get each line of key-values and process the variables for Options;
|
//get each line of key-values and process the variables for Options;
|
||||||
$query = $this->parseUserOptions(SimpleTemplate::parse($options, $obj));
|
$query = $this->parseUserOptions($options, $obj);
|
||||||
|
|
||||||
$client = new \GuzzleHttp\Client();
|
$client = app(\GuzzleHttp\Client::class);
|
||||||
$request_opts['proxy'] = Proxy::forGuzzle();
|
$request_opts['proxy'] = Proxy::forGuzzle();
|
||||||
if (isset($auth) && ! empty($auth[0])) {
|
if (isset($auth) && ! empty($auth[0])) {
|
||||||
$request_opts['auth'] = $auth;
|
$request_opts['auth'] = $auth;
|
||||||
|
49
database/factories/AlertTransportFactory.php
Normal file
49
database/factories/AlertTransportFactory.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\AlertTransport;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use LibreNMS\Alert\Transport;
|
||||||
|
|
||||||
|
class AlertTransportFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = AlertTransport::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'transport_name' => $this->faker->text(30),
|
||||||
|
'transport_type' => $this->faker->randomElement(Transport::list()),
|
||||||
|
'is_default' => 0,
|
||||||
|
'transport_config' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function api(
|
||||||
|
string $options = '',
|
||||||
|
string $method = 'get',
|
||||||
|
string $body = '',
|
||||||
|
string $url = 'https://librenms.org',
|
||||||
|
string $headers = 'test=header',
|
||||||
|
string $username = '',
|
||||||
|
string $password = ''
|
||||||
|
): AlertTransportFactory {
|
||||||
|
$config = [
|
||||||
|
'api-method' => $method,
|
||||||
|
'api-url' => $url,
|
||||||
|
'api-options' => $options,
|
||||||
|
'api-headers' => $headers,
|
||||||
|
'api-body' => $body,
|
||||||
|
'api-auth-username' => $username,
|
||||||
|
'api-auth-password' => $password,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->state(function () use ($config) {
|
||||||
|
return [
|
||||||
|
'transport_type' => 'api',
|
||||||
|
'transport_config' => $config,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -11,7 +11,7 @@
|
|||||||
* the source code distribution for details.
|
* the source code distribution for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use LibreNMS\Config;
|
use LibreNMS\Alert\Transport;
|
||||||
|
|
||||||
if (Auth::user()->hasGlobalAdmin()) {
|
if (Auth::user()->hasGlobalAdmin()) {
|
||||||
?>
|
?>
|
||||||
@@ -42,19 +42,9 @@ if (Auth::user()->hasGlobalAdmin()) {
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Create list of transport
|
// Create list of transport
|
||||||
$transport_dir = Config::get('install_dir') . '/LibreNMS/Alert/Transport';
|
$transports_list = Transport::list();
|
||||||
$transports_list = [];
|
|
||||||
foreach (scandir($transport_dir) as $transport) {
|
|
||||||
$transport = strstr($transport, '.', true);
|
|
||||||
if (empty($transport)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$class = "\LibreNMS\Alert\Transport\\$transport";
|
|
||||||
$instance = new $class;
|
|
||||||
$transports_list[$transport] = $instance->name();
|
|
||||||
}
|
|
||||||
foreach ($transports_list as $transport => $name) {
|
foreach ($transports_list as $transport => $name) {
|
||||||
echo '<option value="' . strtolower($transport) . '-form">' . $name . '</option>';
|
echo '<option value="' . $transport . '-form">' . $name . '</option>';
|
||||||
} ?>
|
} ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,16 +60,16 @@ if (Auth::user()->hasGlobalAdmin()) {
|
|||||||
|
|
||||||
$switches = []; // store names of bootstrap switches
|
$switches = []; // store names of bootstrap switches
|
||||||
foreach ($transports_list as $transport => $name) {
|
foreach ($transports_list as $transport => $name) {
|
||||||
$class = 'LibreNMS\\Alert\\Transport\\' . $transport;
|
$class = Transport::getClass($transport);
|
||||||
|
|
||||||
if (! method_exists($class, 'configTemplate')) {
|
if (! method_exists($class, 'configTemplate')) {
|
||||||
// Skip since support has not been added
|
// Skip since support has not been added
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
echo '<form method="post" role="form" id="' . strtolower($transport) . '-form" class="form-horizontal transport">';
|
echo '<form method="post" role="form" id="' . $transport . '-form" class="form-horizontal transport">';
|
||||||
echo csrf_field();
|
echo csrf_field();
|
||||||
echo '<input type="hidden" name="transport-type" value="' . strtolower($transport) . '">';
|
echo '<input type="hidden" name="transport-type" value="' . $transport . '">';
|
||||||
|
|
||||||
$tmp = call_user_func($class . '::configTemplate');
|
$tmp = call_user_func($class . '::configTemplate');
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ if ($request->has('oauthtransport')) {
|
|||||||
|
|
||||||
if ($validator->passes()) {
|
if ($validator->passes()) {
|
||||||
$transport_name = $request->get('oauthtransport');
|
$transport_name = $request->get('oauthtransport');
|
||||||
$class = 'LibreNMS\\Alert\\Transport\\' . $transport_name;
|
$class = \LibreNMS\Alert\Transport::getClass($transport_name);
|
||||||
if (class_exists($class)) {
|
if (class_exists($class)) {
|
||||||
$transport = app($class);
|
$transport = app($class);
|
||||||
if ($transport->handleOauth($request)) {
|
if ($transport->handleOauth($request)) {
|
||||||
|
80
tests/Traits/MockGuzzleClient.php
Normal file
80
tests/Traits/MockGuzzleClient.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
62
tests/Unit/ApiTransportTest.php
Normal file
62
tests/Unit/ApiTransportTest.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace LibreNMS\Tests\Unit;
|
||||||
|
|
||||||
|
use App\Models\AlertTransport;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
use LibreNMS\Config;
|
||||||
|
use LibreNMS\Tests\TestCase;
|
||||||
|
use LibreNMS\Tests\Traits\MockGuzzleClient;
|
||||||
|
|
||||||
|
class ApiTransportTest extends TestCase
|
||||||
|
{
|
||||||
|
use MockGuzzleClient;
|
||||||
|
|
||||||
|
public function testGetMultilineVariables(): void
|
||||||
|
{
|
||||||
|
/** @var AlertTransport $transport */
|
||||||
|
$transport = AlertTransport::factory()->api('text={{ $msg }}')->make();
|
||||||
|
|
||||||
|
$this->mockGuzzleClient([
|
||||||
|
new Response(200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$obj = ['msg' => "This is a multi-line\nalert."];
|
||||||
|
$opts = Config::get('alert.transports.' . $transport->transport_type);
|
||||||
|
$result = $transport->instance()->deliverAlert($obj, $opts);
|
||||||
|
|
||||||
|
$this->assertTrue($result);
|
||||||
|
|
||||||
|
$history = $this->guzzleRequestHistory();
|
||||||
|
$this->assertCount(1, $history);
|
||||||
|
$this->assertEquals('GET', $history[0]->getMethod());
|
||||||
|
$this->assertEquals('text=This%20is%20a%20multi-line%0Aalert.', $history[0]->getUri()->getQuery());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostMultilineVariables(): void
|
||||||
|
{
|
||||||
|
/** @var AlertTransport $transport */
|
||||||
|
$transport = AlertTransport::factory()->api(
|
||||||
|
'text={{ $msg }}',
|
||||||
|
'post',
|
||||||
|
'bodytext={{ $msg }}',
|
||||||
|
)->make();
|
||||||
|
|
||||||
|
$this->mockGuzzleClient([
|
||||||
|
new Response(200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$obj = ['msg' => "This is a post multi-line\nalert."];
|
||||||
|
$opts = Config::get('alert.transports.' . $transport->transport_type);
|
||||||
|
$result = $transport->instance()->deliverAlert($obj, $opts);
|
||||||
|
|
||||||
|
$this->assertTrue($result);
|
||||||
|
|
||||||
|
$history = $this->guzzleRequestHistory();
|
||||||
|
$this->assertCount(1, $history);
|
||||||
|
$this->assertEquals('POST', $history[0]->getMethod());
|
||||||
|
// FUBAR
|
||||||
|
$this->assertEquals('text=This%20is%20a%20post%20multi-line%0Aalert.', $history[0]->getUri()->getQuery());
|
||||||
|
$this->assertEquals("bodytext=This is a post multi-line\nalert.", (string) $history[0]->getBody());
|
||||||
|
}
|
||||||
|
}
|
@@ -25,22 +25,21 @@
|
|||||||
|
|
||||||
namespace LibreNMS\Tests\Unit\Data;
|
namespace LibreNMS\Tests\Unit\Data;
|
||||||
|
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use GuzzleHttp\Exception\RequestException;
|
use GuzzleHttp\Exception\RequestException;
|
||||||
use GuzzleHttp\Handler\MockHandler;
|
|
||||||
use GuzzleHttp\HandlerStack;
|
|
||||||
use GuzzleHttp\Middleware;
|
|
||||||
use GuzzleHttp\Psr7\Request;
|
use GuzzleHttp\Psr7\Request;
|
||||||
use GuzzleHttp\Psr7\Response;
|
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();
|
||||||
@@ -51,13 +50,12 @@ class PrometheusStoreTest extends TestCase
|
|||||||
|
|
||||||
public function testFailWrite()
|
public function testFailWrite()
|
||||||
{
|
{
|
||||||
$stack = HandlerStack::create(new MockHandler([
|
$this->mockGuzzleClient([
|
||||||
new Response(422, [], 'Bad response'),
|
new Response(422, [], 'Bad response'),
|
||||||
new RequestException('Exception thrown', new Request('POST', 'test')),
|
new RequestException('Exception thrown', new Request('POST', 'test')),
|
||||||
]));
|
]);
|
||||||
|
|
||||||
$client = new Client(['handler' => $stack]);
|
$prometheus = app(Prometheus::class);
|
||||||
$prometheus = new Prometheus($client);
|
|
||||||
|
|
||||||
\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 Exception: Client error: `POST http://fake:9999/metrics/job/librenms/instance/test/measurement/none` resulted in a `422 Unprocessable Entity` response:\nBad response\n");
|
||||||
@@ -68,16 +66,11 @@ class PrometheusStoreTest extends TestCase
|
|||||||
|
|
||||||
public function testSimpleWrite()
|
public function testSimpleWrite()
|
||||||
{
|
{
|
||||||
$stack = HandlerStack::create(new MockHandler([
|
$this->mockGuzzleClient([
|
||||||
new Response(200),
|
new Response(200),
|
||||||
]));
|
]);
|
||||||
|
|
||||||
$container = [];
|
$prometheus = app(Prometheus::class);
|
||||||
$history = Middleware::history($container);
|
|
||||||
|
|
||||||
$stack->push($history);
|
|
||||||
$client = new Client(['handler' => $stack]);
|
|
||||||
$prometheus = new Prometheus($client);
|
|
||||||
|
|
||||||
$device = ['hostname' => 'testhost'];
|
$device = ['hostname' => 'testhost'];
|
||||||
$measurement = 'testmeasure';
|
$measurement = 'testmeasure';
|
||||||
@@ -89,15 +82,12 @@ class PrometheusStoreTest extends TestCase
|
|||||||
|
|
||||||
$prometheus->put($device, $measurement, $tags, $fields);
|
$prometheus->put($device, $measurement, $tags, $fields);
|
||||||
|
|
||||||
$this->assertCount(1, $container, 'Did not receive the expected number of requests');
|
$history = $this->guzzleRequestHistory();
|
||||||
|
$this->assertCount(1, $history, 'Did not receive the expected number of requests');
|
||||||
/** @var Request $request */
|
$this->assertEquals('POST', $history[0]->getMethod());
|
||||||
$request = $container[0]['request'];
|
$this->assertEquals('/metrics/job/librenms/instance/testhost/measurement/testmeasure/ifName/testifname/type/testtype', $history[0]->getUri()->getPath());
|
||||||
|
$this->assertEquals('fake', $history[0]->getUri()->getHost());
|
||||||
$this->assertEquals('POST', $request->getMethod());
|
$this->assertEquals(9999, $history[0]->getUri()->getPort());
|
||||||
$this->assertEquals('/metrics/job/librenms/instance/testhost/measurement/testmeasure/ifName/testifname/type/testtype', $request->getUri()->getPath());
|
$this->assertEquals("ifIn 234234\nifOut 53453\n", (string) $history[0]->getBody());
|
||||||
$this->assertEquals('fake', $request->getUri()->getHost());
|
|
||||||
$this->assertEquals(9999, $request->getUri()->getPort());
|
|
||||||
$this->assertEquals("ifIn 234234\nifOut 53453\n", (string) $request->getBody());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user