Disable plugins that have errors (#14383)

* Disable plugins that have errors
Disable plugin if a hook throws an error and set a notification
Move notification code to class, so we can access it
Clear notification when plugin is attempted to be enabled again

* fix style and lint fixes

* another lint fix and handle if property is missing
This commit is contained in:
Tony Murray 2022-09-25 22:47:58 -05:00 committed by GitHub
parent 333ba7c2cd
commit e990dfcb35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 222 additions and 197 deletions

View File

@ -27,6 +27,7 @@
namespace LibreNMS;
use App\Models\Plugin;
use LibreNMS\Util\Notifications;
use Log;
/**
@ -190,8 +191,14 @@ class Plugins
} else {
@call_user_func_array([$plugin, $hook], $params);
}
} catch (\Exception $e) {
} catch (\Exception|\Error $e) {
Log::error($e);
$class = (string) get_class($plugin);
$name = property_exists($class, 'name') ? $class::$name : basename(str_replace('\\', '/', $class));
Notifications::create("Plugin $name disabled", "$name caused an error and was disabled, please check with the plugin creator to fix the error. The error can be found in logs/librenms.log", 'plugins', 2);
Plugin::where('plugin_name', $name)->update(['plugin_active' => 0]);
}
}
}

View File

@ -0,0 +1,153 @@
<?php
/**
* Notifications.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 2015 Daniel Preussker, QuxLabs UG
* @copyright 2022 Tony Murray
* @author Daniel Preussker
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Util;
use App\Models\Notification;
use Illuminate\Support\Arr;
use LibreNMS\Config;
class Notifications
{
/**
* Post notifications to users
*/
public static function post(): void
{
$notifications = self::fetch();
echo '[ ' . date('r') . ' ] Updating DB ';
foreach ($notifications as $notif) {
if (! Notification::where('checksum', $notif['checksum'])->exists()) {
Notification::create($notif);
echo '.';
}
}
echo ' Done' . PHP_EOL;
}
/**
* Create a new custom notification. Duplicate title+message notifications will not be created.
*
* @param string $title
* @param string $message
* @param string $source A string describing what created this notification
* @param int $severity 0=ok, 1=warning, 2=critical
* @param string|null $date
* @return bool
*/
public static function create(string $title, string $message, string $source, int $severity = 0, ?string $date = null): bool
{
$checksum = hash('sha512', $title . $message);
return Notification::firstOrCreate([
'checksum' => $checksum,
], [
'title' => $title,
'body' => $message,
'severity' => $severity,
'source' => $source,
'checksum' => $checksum,
'datetime' => date('Y-m-d', is_null($date) ? time() : strtotime($date)),
])->wasRecentlyCreated;
}
/**
* Removes all notifications with the given title.
* This should be used with care.
*/
public static function remove(string $title): void
{
Notification::where('title', $title)->get()->each->delete();
}
/**
* Pull notifications from remotes
*
* @return array Notifications
*/
protected static function fetch(): array
{
$notifications = [];
foreach (Config::get('notifications') as $name => $url) {
echo '[ ' . date('r') . " ] $name $url ";
$feed = json_decode(json_encode(simplexml_load_string(file_get_contents($url))), true);
$feed = isset($feed['channel']) ? self::parseRss($feed) : self::parseAtom($feed);
array_walk($feed, function (&$items, $key, $url) {
$items['source'] = $url;
}, $url);
$notifications = array_merge($notifications, $feed);
echo '(' . count($notifications) . ')' . PHP_EOL;
}
return Arr::sort($notifications, 'datetime');
}
protected static function parseRss(array $feed): array
{
$obj = [];
if (! array_key_exists('0', $feed['channel']['item'])) {
$feed['channel']['item'] = [$feed['channel']['item']];
}
foreach ($feed['channel']['item'] as $item) {
$obj[] = [
'title' => $item['title'],
'body' => $item['description'],
'checksum' => hash('sha512', $item['title'] . $item['description']),
'datetime' => date('Y-m-d', strtotime($item['pubDate']) ?: time()),
];
}
return $obj;
}
/**
* Parse Atom
*
* @param array $feed Atom Object
* @return array Parsed Object
*/
protected static function parseAtom(array $feed): array
{
$obj = [];
if (! array_key_exists('0', $feed['entry'])) {
$feed['entry'] = [$feed['entry']];
}
foreach ($feed['entry'] as $item) {
$obj[] = [
'title' => $item['title'],
'body' => $item['content'],
'checksum' => hash('sha512', $item['title'] . $item['content']),
'datetime' => date('Y-m-d', strtotime($item['updated']) ?: time()),
];
}
return $obj;
}
}

View File

@ -6,6 +6,7 @@ use App\Models\Plugin;
use App\Plugins\Hooks\SettingsHook;
use App\Plugins\PluginManager;
use Illuminate\Http\Request;
use LibreNMS\Util\Notifications;
class PluginSettingsController extends Controller
{
@ -31,12 +32,17 @@ class PluginSettingsController extends Controller
public function update(Request $request, Plugin $plugin): \Illuminate\Http\RedirectResponse
{
$validated = $this->validate($request, [
$plugin->fill($this->validate($request, [
'plugin_active' => 'in:0,1',
'settings' => 'array',
]);
]));
$plugin->fill($validated)->save();
if ($plugin->isDirty('plugin_active') && $plugin->plugin_active == 1) {
// enabling plugin delete notifications assuming they are fixed
Notifications::remove("Plugin $plugin->plugin_name disabled");
}
$plugin->save();
return redirect()->back();
}

View File

@ -28,6 +28,24 @@ class Notification extends Model
* @var string
*/
protected $primaryKey = 'notifications_id';
protected $fillable = [
'title',
'body',
'severity',
'source',
'checksum',
'datetime',
];
public static function boot()
{
parent::boot();
// delete attribs for this notification
static::deleting(function (Notification $notification) {
$notification->attribs()->delete();
});
}
// ---- Helper Functions ----

View File

@ -30,6 +30,7 @@ use App\Models\Plugin;
use Exception;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use LibreNMS\Util\Notifications;
use Log;
class PluginManager
@ -107,16 +108,22 @@ class PluginManager
*/
public function call(string $hookType, array $args = [], ?string $plugin = null): Collection
{
try {
return $this->hooksFor($hookType, $args, $plugin)
->map(function ($hook) use ($args) {
return $this->hooksFor($hookType, $args, $plugin)
->map(function ($hook) use ($args, $hookType) {
try {
return app()->call([$hook['instance'], 'handle'], $this->fillArgs($args, $hook['plugin_name']));
});
} catch (Exception $e) {
Log::error("Error calling hook $hookType: " . $e->getMessage());
} catch (Exception|\Error $e) {
$name = $hook['plugin_name'];
Log::error("Error calling hook $hookType for $name: " . $e->getMessage());
return new Collection;
}
Notifications::create("Plugin $name disabled", "$name caused an error and was disabled, please check with the plugin creator to fix the error. The error can be found in logs/librenms.log", 'plugins', 2);
Plugin::where('plugin_name', $name)->update(['plugin_active' => 0]);
return 'HOOK FAILED';
}
})->filter(function ($hook) {
return $hook === 'HOOK FAILED';
});
}
/**

View File

@ -22,6 +22,7 @@
"ext-pcre": "*",
"ext-pdo": "*",
"ext-session": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-zlib": "*",
"amenadiel/jpgraph": "^4",

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",
"This file is @generated automatically"
],
"content-hash": "e21d9022b60ccf67f7b0c0b622348ced",
"content-hash": "5f22d1ad8c4de6777b2690d0115f7afe",
"packages": [
{
"name": "amenadiel/jpgraph",

View File

@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Collection;
use LibreNMS\Alert\AlertDB;
use LibreNMS\Config;
use LibreNMS\Util\Debug;
use LibreNMS\Util\Notifications;
use LibreNMS\Validations\Php;
$options = getopt('df:o:t:r:');
@ -39,7 +40,6 @@ if ($options['f'] === 'composer_get_plugins') {
*/
$init_modules = ['alerts'];
require __DIR__ . '/includes/init.php';
include_once __DIR__ . '/includes/notifications.php';
if (isset($options['d'])) {
echo "DEBUG\n";
@ -158,16 +158,14 @@ if ($options['f'] === 'handle_notifiable') {
if ($options['r']) {
// result was a success (1), remove the notification
remove_notification($title);
Notifications::remove($title);
} else {
// result was a failure (0), create the notification
new_notification(
$title,
"The daily update script (daily.sh) has failed on $poller_name."
Notifications::create($title, "The daily update script (daily.sh) has failed on $poller_name."
. 'Please check output by hand. If you need assistance, '
. 'visit the <a href="https://www.librenms.org/#support">LibreNMS Website</a> to find out how.',
2,
'daily.sh'
'daily.sh',
2
);
}
} elseif ($options['t'] === 'phpver') {
@ -183,17 +181,16 @@ if ($options['f'] === 'handle_notifiable') {
$eol_date = Php::PHP_MIN_VERSION_DATE;
}
if (isset($phpver)) {
new_notification(
$error_title,
Notifications::create($error_title,
"PHP version $phpver is the minimum supported version as of $eol_date. We recommend you update to PHP a supported version of PHP (" . Php::PHP_RECOMMENDED_VERSION . ' suggested) to continue to receive updates. If you do not update PHP, LibreNMS will continue to function but stop receiving bug fixes and updates.',
2,
'daily.sh'
'daily.sh',
2
);
exit(1);
}
}
remove_notification($error_title);
Notifications::remove($error_title);
exit(0);
} elseif ($options['t'] === 'pythonver') {
$error_title = 'Error: Python requirements not met';
@ -201,25 +198,23 @@ if ($options['f'] === 'handle_notifiable') {
// if update is not set to false and version is min or newer
if (Config::get('update') && $options['r']) {
if ($options['r'] === 'python3-missing') {
new_notification(
$error_title,
Notifications::create($error_title,
'Python 3 is required to run LibreNMS as of May, 2020. You need to install Python 3 to continue to receive updates. If you do not install Python 3 and required packages, LibreNMS will continue to function but stop receiving bug fixes and updates.',
2,
'daily.sh'
'daily.sh',
2
);
exit(1);
} elseif ($options['r'] === 'python3-deps') {
new_notification(
$error_title,
Notifications::create($error_title,
'Python 3 dependencies are missing. You need to install them via pip3 install -r requirements.txt or system packages to continue to receive updates. If you do not install Python 3 and required packages, LibreNMS will continue to function but stop receiving bug fixes and updates.',
2,
'daily.sh'
'daily.sh',
2
);
exit(1);
}
}
remove_notification($error_title);
Notifications::remove($error_title);
exit(0);
}
}
@ -227,7 +222,7 @@ if ($options['f'] === 'handle_notifiable') {
if ($options['f'] === 'notifications') {
$lock = Cache::lock('notifications', 86000);
if ($lock->get()) {
post_notifications();
Notifications::post();
$lock->release();
}
}

View File

@ -1,162 +0,0 @@
<?php
/* Copyright (C) 2015 Daniel Preussker, QuxLabs UG <preussker@quxlabs.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/>. */
/**
* Notification Poller
*
* @copyright 2015 Daniel Preussker, QuxLabs UG
* @copyright 2017 Tony Murray
* @author Daniel Preussker
* @author Tony Murray <murraytony@gmail.com>
* @license GPL
*
* @link https://www.librenms.org
*/
/**
* Pull notifications from remotes
*
* @return array Notifications
*/
function get_notifications()
{
$obj = [];
foreach (\LibreNMS\Config::get('notifications') as $name => $url) {
echo '[ ' . date('r') . ' ] ' . $url . ' ';
$feed = json_decode(json_encode(simplexml_load_string(file_get_contents($url))), true);
if (isset($feed['channel'])) {
$feed = parse_rss($feed);
} else {
$feed = parse_atom($feed);
}
array_walk($feed, function (&$items, $key, $url) {
$items['source'] = $url;
}, $url);
$obj = array_merge($obj, $feed);
echo '(' . sizeof($obj) . ')' . PHP_EOL;
}
$obj = array_sort_by_column($obj, 'datetime');
return $obj;
}
/**
* Post notifications to users
*
* @return null
*/
function post_notifications()
{
$notifs = get_notifications();
echo '[ ' . date('r') . ' ] Updating DB ';
foreach ($notifs as $notif) {
if (dbFetchCell('select 1 from notifications where checksum = ?', [$notif['checksum']]) != 1 && dbInsert($notif, 'notifications') > 0) {
echo '.';
}
}
echo ' Done';
echo PHP_EOL;
}
/**
* Parse RSS
*
* @param array $feed RSS Object
* @return array Parsed Object
*/
function parse_rss($feed)
{
$obj = [];
if (! array_key_exists('0', $feed['channel']['item'])) {
$feed['channel']['item'] = [$feed['channel']['item']];
}
foreach ($feed['channel']['item'] as $item) {
$obj[] = [
'title'=>$item['title'],
'body'=>$item['description'],
'checksum'=>hash('sha512', $item['title'] . $item['description']),
'datetime'=>date('Y-m-d', strtotime($item['pubDate']) ?: time()),
];
}
return $obj;
}
/**
* Parse Atom
*
* @param array $feed Atom Object
* @return array Parsed Object
*/
function parse_atom($feed)
{
$obj = [];
if (! array_key_exists('0', $feed['entry'])) {
$feed['entry'] = [$feed['entry']];
}
foreach ($feed['entry'] as $item) {
$obj[] = [
'title'=>$item['title'],
'body'=>$item['content'],
'checksum'=>hash('sha512', $item['title'] . $item['content']),
'datetime'=>date('Y-m-d', strtotime($item['updated']) ?: time()),
];
}
return $obj;
}
/**
* Create a new custom notification. Duplicate title+message notifications will not be created.
*
* @param string $title
* @param string $message
* @param int $severity 0=ok, 1=warning, 2=critical
* @param string $source A string describing what created this notification
* @param string $date
* @return bool
*/
function new_notification($title, $message, $severity = 0, $source = 'adhoc', $date = null)
{
$notif = [
'title' => $title,
'body' => $message,
'severity' => $severity,
'source' => $source,
'checksum' => hash('sha512', $title . $message),
'datetime' => date('Y-m-d', is_null($date) ? time() : strtotime($date)),
];
if (dbFetchCell('SELECT 1 FROM `notifications` WHERE `checksum` = ?', [$notif['checksum']]) != 1) {
return dbInsert($notif, 'notifications') > 0;
}
return false;
}
/**
* Removes all notifications with the given title.
* This should be used with care.
*
* @param string $title
*/
function remove_notification($title)
{
$ids = dbFetchColumn('SELECT `notifications_id` FROM `notifications` WHERE `title`=?', [$title]);
foreach ($ids as $id) {
dbDelete('notifications', '`notifications_id`=?', [$id]);
dbDelete('notifications_attribs', '`notifications_id`=?', [$id]);
}
}