From e990dfcb359badc9cba75ee43499e8772bc323a6 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Sun, 25 Sep 2022 22:47:58 -0500 Subject: [PATCH] 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 --- LibreNMS/Plugins.php | 9 +- LibreNMS/Util/Notifications.php | 153 +++++++++++++++++ .../Controllers/PluginSettingsController.php | 12 +- app/Models/Notification.php | 18 ++ app/Plugins/PluginManager.php | 23 ++- composer.json | 1 + composer.lock | 2 +- daily.php | 39 ++--- includes/notifications.php | 162 ------------------ 9 files changed, 222 insertions(+), 197 deletions(-) create mode 100644 LibreNMS/Util/Notifications.php delete mode 100644 includes/notifications.php diff --git a/LibreNMS/Plugins.php b/LibreNMS/Plugins.php index 7fe9eb7416..8cdcc99bba 100644 --- a/LibreNMS/Plugins.php +++ b/LibreNMS/Plugins.php @@ -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]); } } } diff --git a/LibreNMS/Util/Notifications.php b/LibreNMS/Util/Notifications.php new file mode 100644 index 0000000000..b3618e8a00 --- /dev/null +++ b/LibreNMS/Util/Notifications.php @@ -0,0 +1,153 @@ +. + * + * @link https://www.librenms.org + * + * @copyright 2015 Daniel Preussker, QuxLabs UG + * @copyright 2022 Tony Murray + * @author Daniel Preussker + * @author Tony Murray + */ + +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; + } +} diff --git a/app/Http/Controllers/PluginSettingsController.php b/app/Http/Controllers/PluginSettingsController.php index c01437540a..1e47c9d02d 100644 --- a/app/Http/Controllers/PluginSettingsController.php +++ b/app/Http/Controllers/PluginSettingsController.php @@ -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(); } diff --git a/app/Models/Notification.php b/app/Models/Notification.php index 68d1d896be..d4bbe8059d 100644 --- a/app/Models/Notification.php +++ b/app/Models/Notification.php @@ -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 ---- diff --git a/app/Plugins/PluginManager.php b/app/Plugins/PluginManager.php index 69d683cdbd..247537b004 100644 --- a/app/Plugins/PluginManager.php +++ b/app/Plugins/PluginManager.php @@ -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'; + }); } /** diff --git a/composer.json b/composer.json index 0dc7b6a452..616a9e7b49 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "ext-pcre": "*", "ext-pdo": "*", "ext-session": "*", + "ext-simplexml": "*", "ext-xml": "*", "ext-zlib": "*", "amenadiel/jpgraph": "^4", diff --git a/composer.lock b/composer.lock index a8183232d5..89ba552ad2 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/daily.php b/daily.php index 6ec79e7d84..05d7631a49 100644 --- a/daily.php +++ b/daily.php @@ -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 LibreNMS Website 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(); } } diff --git a/includes/notifications.php b/includes/notifications.php deleted file mode 100644 index 5035e3e34d..0000000000 --- a/includes/notifications.php +++ /dev/null @@ -1,162 +0,0 @@ - - * 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 . */ - -/** - * Notification Poller - * - * @copyright 2015 Daniel Preussker, QuxLabs UG - * @copyright 2017 Tony Murray - * @author Daniel Preussker - * @author Tony Murray - * @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]); - } -}