Better handling of some alerting errors (#13446)

* Better handling of some alerting errors

* Better error output

* Consolidate simple template parsing

* Fixes reported by phpstan (one was a bug, yay!)

* add back forgotten trim

* don't remove the template if there is no match

* Match previous behavior, which was inconsistent.

* use anonymous class for tests instead

* Oopsie, Stringable is PHP8+

* fix style
This commit is contained in:
Tony Murray 2021-10-29 22:12:20 -05:00 committed by GitHub
parent 99d2462b80
commit 2c77edf4d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 187 additions and 124 deletions

View File

@ -93,19 +93,19 @@ class AlertUtil
$uids = [];
foreach ($results as $result) {
$tmp = null;
if (is_numeric($result['bill_id'])) {
if (isset($result['bill_id']) && is_numeric($result['bill_id'])) {
$tmpa = dbFetchRows('SELECT user_id FROM bill_perms WHERE bill_id = ?', [$result['bill_id']]);
foreach ($tmpa as $tmp) {
$uids[$tmp['user_id']] = $tmp['user_id'];
}
}
if (is_numeric($result['port_id'])) {
if (isset($result['port_id']) && is_numeric($result['port_id'])) {
$tmpa = dbFetchRows('SELECT user_id FROM ports_perms WHERE port_id = ?', [$result['port_id']]);
foreach ($tmpa as $tmp) {
$uids[$tmp['user_id']] = $tmp['user_id'];
}
}
if (is_numeric($result['device_id'])) {
if (isset($result['device_id']) && is_numeric($result['device_id'])) {
if (Config::get('alert.syscontact') == true) {
if (dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_bool' AND device_id = ?", [$result['device_id']])) {
$tmpa = dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_string' AND device_id = ?", [$result['device_id']]);

View File

@ -65,12 +65,12 @@ abstract class Transport implements TransportInterface
* @param string $input
* @return array
*/
protected function parseUserOptions($input)
protected function parseUserOptions(string $input): array
{
$options = [];
foreach (explode(PHP_EOL, $input) as $option) {
foreach (preg_split('/\\r\\n|\\r|\\n/', $input, -1, PREG_SPLIT_NO_EMPTY) as $option) {
if (Str::contains($option, '=')) {
[$k,$v] = explode('=', $option, 2);
[$k, $v] = explode('=', $option, 2);
$options[$k] = trim($v);
}
}

View File

@ -24,6 +24,7 @@
namespace LibreNMS\Alert\Transport;
use App\View\SimpleTemplate;
use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy;
@ -46,31 +47,14 @@ class Api extends Transport
private function contactAPI($obj, $api, $options, $method, $auth, $headers, $body)
{
$request_opts = [];
$request_heads = [];
$query = [];
$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;
foreach (preg_split('/\\r\\n|\\r|\\n/', $headers, -1, PREG_SPLIT_NO_EMPTY) as $current_line) {
[$u_key, $u_val] = explode('=', $current_line, 2);
foreach ($obj as $p_key => $p_val) {
$u_val = str_replace('{{ $' . $p_key . ' }}', $p_val, $u_val);
}
//store the parameter in the array for HTTP headers
$request_heads[$u_key] = $u_val;
}
$request_heads = $this->parseUserOptions(SimpleTemplate::parse($headers, $obj));
//get each line of key-values and process the variables for Options;
foreach (preg_split('/\\r\\n|\\r|\\n/', $options, -1, PREG_SPLIT_NO_EMPTY) as $current_line) {
[$u_key, $u_val] = explode('=', $current_line, 2);
// Replace the values
foreach ($obj as $p_key => $p_val) {
$u_val = str_replace('{{ $' . $p_key . ' }}', $p_val, $u_val);
}
//store the parameter in the array for HTTP query
$query[$u_key] = $u_val;
}
$query = $this->parseUserOptions(SimpleTemplate::parse($options, $obj));
$client = new \GuzzleHttp\Client();
$request_opts['proxy'] = Proxy::forGuzzle();
@ -89,10 +73,7 @@ class Api extends Transport
$res = $client->request('PUT', $host, $request_opts);
} else { //Method POST
$request_opts['query'] = $query;
foreach ($obj as $metric => $value) {
$body = str_replace('{{ $' . $metric . ' }}', $value, $body);
}
$request_opts['body'] = $body;
$request_opts['body'] = SimpleTemplate::parse($body, $obj);
$res = $client->request('POST', $host, $request_opts);
}

View File

@ -23,6 +23,7 @@
namespace LibreNMS\Alert\Transport;
use App\View\SimpleTemplate;
use LibreNMS\Alert\Transport;
use LibreNMS\Util\Proxy;
@ -51,9 +52,7 @@ class Matrix extends Transport
$request_heads['Content-Type'] = 'application/json';
$request_heads['Accept'] = 'application/json';
foreach ($obj as $p_key => $p_val) {
$message = str_replace('{{ $' . $p_key . ' }}', $p_val, $message);
}
$message = SimpleTemplate::parse($message, $obj);
$body = ['body'=>$message, 'msgtype'=>'m.text'];

View File

@ -28,6 +28,8 @@ use LibreNMS\Util\Proxy;
class Rocket extends Transport
{
protected $name = 'Rocket Chat';
public function deliverAlert($obj, $opts)
{
$rocket_opts = $this->parseUserOptions($this->config['rocket-options']);
@ -51,10 +53,10 @@ class Rocket extends Transport
'text' => $rocket_msg,
],
],
'channel' => $api['channel'],
'username' => $api['username'],
'icon_url' => $api['icon_url'],
'icon_emoji' => $api['icon_emoji'],
'channel' => $api['channel'] ?? null,
'username' => $api['username'] ?? null,
'icon_url' => $api['icon_url'] ?? null,
'icon_emoji' => $api['icon_emoji'] ?? null,
];
$alert_message = json_encode($data);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);

View File

@ -25,6 +25,7 @@
namespace LibreNMS\Device;
use App\View\SimpleTemplate;
use Cache;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
@ -166,35 +167,29 @@ class YamlDiscovery
$value = static::getValueFromData($name, $index, $def, $pre_cache);
if (is_null($value)) {
// built in replacements
$search = [
'{{ $index }}',
'{{ $count }}',
// basic replacements
$variables = [
'index' => $index,
'count' => $count,
];
$replace = [
$index,
$count,
];
// prepare the $subindexX match variable replacement
foreach (explode('.', $index) as $pos => $subindex) {
$search[] = '{{ $subindex' . $pos . ' }}';
$replace[] = $subindex;
$variables['subindex' . $pos] = $subindex;
}
$value = str_replace($search, $replace, $def[$name] ?? '');
$value = (string) (new SimpleTemplate($def[$name] ?? '', $variables))->keepEmptyTemplates();
// search discovery data for values
$value = preg_replace_callback('/{{ \$?([a-zA-Z0-9\-.:]+) }}/', function ($matches) use ($index, $def, $pre_cache) {
$replace = static::getValueFromData($matches[1], $index, $def, $pre_cache, null);
$template = new SimpleTemplate($value);
$template->replaceWith(function ($matches) use ($index, $def, $pre_cache) {
$replace = static::getValueFromData($matches[1], $index, $def, $pre_cache);
if (is_null($replace)) {
d_echo('Warning: No variable available to replace ' . $matches[1] . ".\n");
\Log::warning('YamlDiscovery: No variable available to replace ' . $matches[1]);
return ''; // remove the unavailable variable
}
return $replace;
}, $value);
});
$value = (string) $template;
}
return $value;

View File

@ -27,6 +27,7 @@ namespace LibreNMS\OS\Traits;
use App\Models\Device;
use App\Models\Location;
use App\View\SimpleTemplate;
use Illuminate\Support\Arr;
use LibreNMS\Util\StringHelpers;
use Log;
@ -75,7 +76,7 @@ trait YamlOSDiscovery
}
$device->$field = isset($os_yaml["{$field}_template"])
? $this->parseTemplate($os_yaml["{$field}_template"], $data)
? trim(SimpleTemplate::parse($os_yaml["{$field}_template"], $data))
: $value;
}
}
@ -131,13 +132,6 @@ trait YamlOSDiscovery
}
}
private function parseTemplate($template, $data)
{
return trim(preg_replace_callback('/{{ ([^ ]+) }}/', function ($matches) use ($data) {
return $data[$matches[1]] ?? '';
}, $template));
}
private function translateSysObjectID($mib, $regex)
{
$device = $this->getDevice();

View File

@ -146,4 +146,15 @@ class StringHelpers
return $namespace . $class;
}
/**
* Check if variable can be cast to a string
*
* @param mixed $var
* @return bool
*/
public static function isStringable($var): bool
{
return $var === null || is_scalar($var) || (is_object($var) && method_exists($var, '__toString'));
}
}

View File

@ -44,7 +44,8 @@ class AlertTransportController extends Controller
return response()->json(['status' => 'ok']);
}
} catch (\Exception $e) {
$result = $e->getMessage();
\Log::error($e);
$result = basename($e->getFile(), '.php') . ':' . $e->getLine() . ' ' . $e->getMessage();
}
return response()->json([

113
app/View/SimpleTemplate.php Normal file
View File

@ -0,0 +1,113 @@
<?php
/*
* SimpleTemplate.php
*
* Simple variable substitution template
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\View;
use LibreNMS\Util\StringHelpers;
class SimpleTemplate
{
/**
* @var string
*/
private $template;
/**
* @var array
*/
private $variables;
/**
* @var string
*/
private $regex = '/{{ \$?([a-zA-Z0-9\-_.:]+) }}/';
/**
* @var callable
*/
private $callback;
/**
* @var bool
*/
private $keepEmpty = false;
public function __construct(string $template, array $variables = [])
{
$this->template = $template;
$this->variables = $variables;
}
/**
* By default, unmatched templates will be removed from the output, set this to keep them
*/
public function keepEmptyTemplates(): SimpleTemplate
{
$this->keepEmpty = true;
return $this;
}
/**
* Add a variable to the set of possible substitutions
*/
public function setVariable(string $key, string $value): SimpleTemplate
{
$this->variables[$key] = $value;
return $this;
}
/**
* Instead of using the given variables to replace {{ var }}
* send the matched variable to this callback, which will return a string to replace it
*/
public function replaceWith(callable $callback): SimpleTemplate
{
$this->callback = $callback;
return $this;
}
/**
* Create and parse a simple template
*
* @param string $template
* @param array $variables
* @return string
*/
public static function parse(string $template, array $variables): string
{
return (string) new static($template, $variables);
}
public function __toString()
{
return preg_replace_callback($this->regex, $this->callback ?? function ($matches) {
$replacement = $this->variables[$matches[1]] ?? ($this->keepEmpty ? $matches[0] : '');
if (! StringHelpers::isStringable($replacement)) {
return '';
}
return $replacement;
}, $this->template);
}
}

View File

@ -3820,21 +3820,6 @@ parameters:
count: 1
path: LibreNMS/OS.php
-
message: "#^Method LibreNMS\\\\OS\\:\\:parseTemplate\\(\\) has no return typehint specified\\.$#"
count: 1
path: LibreNMS/OS.php
-
message: "#^Method LibreNMS\\\\OS\\:\\:parseTemplate\\(\\) has parameter \\$data with no typehint specified\\.$#"
count: 1
path: LibreNMS/OS.php
-
message: "#^Method LibreNMS\\\\OS\\:\\:parseTemplate\\(\\) has parameter \\$template with no typehint specified\\.$#"
count: 1
path: LibreNMS/OS.php
-
message: "#^Method LibreNMS\\\\OS\\:\\:persistGraphs\\(\\) has no return typehint specified\\.$#"
count: 1
@ -4375,21 +4360,6 @@ parameters:
count: 1
path: LibreNMS/OS/Shared/Cisco.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Cisco\\:\\:parseTemplate\\(\\) has no return typehint specified\\.$#"
count: 1
path: LibreNMS/OS/Shared/Cisco.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Cisco\\:\\:parseTemplate\\(\\) has parameter \\$data with no typehint specified\\.$#"
count: 1
path: LibreNMS/OS/Shared/Cisco.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Cisco\\:\\:parseTemplate\\(\\) has parameter \\$template with no typehint specified\\.$#"
count: 1
path: LibreNMS/OS/Shared/Cisco.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Cisco\\:\\:pollSlas\\(\\) has no return typehint specified\\.$#"
count: 1
@ -4530,21 +4500,6 @@ parameters:
count: 1
path: LibreNMS/OS/Shared/Unix.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Unix\\:\\:parseTemplate\\(\\) has no return typehint specified\\.$#"
count: 1
path: LibreNMS/OS/Shared/Unix.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Unix\\:\\:parseTemplate\\(\\) has parameter \\$data with no typehint specified\\.$#"
count: 1
path: LibreNMS/OS/Shared/Unix.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Unix\\:\\:parseTemplate\\(\\) has parameter \\$template with no typehint specified\\.$#"
count: 1
path: LibreNMS/OS/Shared/Unix.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Unix\\:\\:translateSysObjectID\\(\\) has no return typehint specified\\.$#"
count: 1
@ -4625,21 +4580,6 @@ parameters:
count: 1
path: LibreNMS/OS/Shared/Zyxel.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Zyxel\\:\\:parseTemplate\\(\\) has no return typehint specified\\.$#"
count: 1
path: LibreNMS/OS/Shared/Zyxel.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Zyxel\\:\\:parseTemplate\\(\\) has parameter \\$data with no typehint specified\\.$#"
count: 1
path: LibreNMS/OS/Shared/Zyxel.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Zyxel\\:\\:parseTemplate\\(\\) has parameter \\$template with no typehint specified\\.$#"
count: 1
path: LibreNMS/OS/Shared/Zyxel.php
-
message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Zyxel\\:\\:translateSysObjectID\\(\\) has no return typehint specified\\.$#"
count: 1

View File

@ -35,7 +35,7 @@ class StringHelperTest extends TestCase
*
* @return void
*/
public function testInferEncoding()
public function testInferEncoding(): void
{
$this->assertEquals(null, StringHelpers::inferEncoding(null));
$this->assertEquals('', StringHelpers::inferEncoding(''));
@ -48,4 +48,31 @@ class StringHelperTest extends TestCase
config(['app.charset' => 'Shift_JIS']);
$this->assertEquals('コンサート', StringHelpers::inferEncoding(base64_decode('g1KDk4NUgVuDZw==')));
}
public function testIsStringable(): void
{
$this->assertTrue(StringHelpers::isStringable(null));
$this->assertTrue(StringHelpers::isStringable(''));
$this->assertTrue(StringHelpers::isStringable('string'));
$this->assertTrue(StringHelpers::isStringable(-1));
$this->assertTrue(StringHelpers::isStringable(1.0));
$this->assertTrue(StringHelpers::isStringable(false));
$this->assertFalse(StringHelpers::isStringable([]));
$this->assertFalse(StringHelpers::isStringable((object) []));
$stringable = new class
{
public function __toString()
{
return '';
}
};
$this->assertTrue(StringHelpers::isStringable($stringable));
$nonstringable = new class
{
};
$this->assertFalse(StringHelpers::isStringable($nonstringable));
}
}