Repository: nautobot/nautobot

Issue:
Plugin Load Failure
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Python version: 3.8.5
* Nautobot version: 1.0.0b1

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. `pip install nautobot-netbox-importer`
2. Enable the plugin in `nautobot_config.py`:
     `PLUGINS = ["nautobot_netbox_importer"]`
3. `sudo systemctl restart nautobot`
4. Browse to the nautobot web User Interface

<!-- What did you expect to happen? -->
### Expected Behavior
Expected to see the nautobot landing page on the UI

<!-- What happened instead? -->
### Observed Behavior
**Internal Server Error** displayed on UI

Gunicorn reports errors in the nautobot logs.
```
(nautobot) nautobot@ubuntu-20-04:~$ sudo journalctl -eu nautobot.service
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/opt/nautobot/lib/python3.8/site-packages/django/core/handlers/wsgi.py", line 133, in __call__
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     response = self.get_response(request)
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/opt/nautobot/lib/python3.8/site-packages/django/core/handlers/base.py", line 130, in get_response
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     response = self._middleware_chain(request)
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/opt/nautobot/lib/python3.8/site-packages/django/core/handlers/exception.py", line 49, in inner
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     response = response_for_exception(request, exc)
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/opt/nautobot/lib/python3.8/site-packages/django/core/handlers/exception.py", line 103, in response_for_exception
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/opt/nautobot/lib/python3.8/site-packages/django/core/handlers/exception.py", line 141, in handle_uncaught_exception
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     callback, param_dict = resolver.resolve_error_handler(500)
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/opt/nautobot/lib/python3.8/site-packages/django/urls/resolvers.py", line 602, in resolve_error_handler
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     callback = getattr(self.urlconf_module, 'handler%s' % view_type, None)
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/opt/nautobot/lib/python3.8/site-packages/django/utils/functional.py", line 48, in __get__
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     res = instance.__dict__[self.name] = self.func(instance)
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/opt/nautobot/lib/python3.8/site-packages/django/urls/resolvers.py", line 582, in urlconf_module
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     return import_module(self.urlconf_name)
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/usr/lib/python3.8/importlib/__init__.py", line 127, in import_module
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     return _bootstrap._gcd_import(name[level:], package, level)
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "<frozen importlib._bootstrap>", line 991, in _find_and_load
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "<frozen importlib._bootstrap_external>", line 783, in exec_module
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/opt/nautobot/lib/python3.8/site-packages/nautobot/core/urls.py", line 12, in <module>
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     from nautobot.extras.plugins.urls import (
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/opt/nautobot/lib/python3.8/site-packages/nautobot/extras/plugins/urls.py", line 32, in <module>
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     app = apps.get_app_config(plugin_name)
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:   File "/opt/nautobot/lib/python3.8/site-packages/django/apps/registry.py", line 163, in get_app_config
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]:     raise LookupError(message)
Mar 05 11:52:52 ubuntu-20-04 gunicorn[25549]: LookupError: No installed app with label 'nautobot_netbox_importer'.
```





Discussion:
Also tried with plugin `nautobot-device-onboarding`. Same result.
Just for some context, was troubleshooting this last night with @jvanderaa and we isolated it to be directly related to Gunicorn for "some" reason.
Mirroring the comment here from #88 for those with vested interest:

This addresses the issue with #95 by circumventing it entirely, but it does not fix the underlying issue with WHY the plugins are not being loaded correctly. That will take more time, but this will alleviate the problem for the near term by launching uWSGI from within the server utility instead of the other way around, with Gunicorn/uWSGI invoking the application.
@pke11y I still intend to track this to get to the bottom of this issue, but can you please give v1.0.0b2 a shot w/ the revised install instructions to use uWSGI over Gunicorn and let me know how it goes for you? Thanks!
@jathanism no probs. Will try it this evening.
@jathanism tried b2 version w/ uWSGI and loaded the `nautobot_netbox_importer`. Install went well and plugin loaded. Nice job!
@pke11y Great, thanks! That means we've successfully worked around this issue for now while creating new ones. :) 

Resolving patch:
diff --git a/nautobot/core/cli.py b/nautobot/core/cli.py
--- a/nautobot/core/cli.py
+++ b/nautobot/core/cli.py
@@ -223,11 +223,4 @@ def _setting(name, default=None):
 
     # Process the plugins and manipulate the specified config settings that are
     # passed in.
-    load_plugins(
-        settings.PLUGINS,
-        settings.INSTALLED_APPS,
-        settings.PLUGINS_CONFIG,
-        settings.VERSION,
-        settings.MIDDLEWARE,
-        settings.CACHEOPS,
-    )
+    load_plugins(settings)
diff --git a/nautobot/extras/plugins/utils.py b/nautobot/extras/plugins/utils.py
--- a/nautobot/extras/plugins/utils.py
+++ b/nautobot/extras/plugins/utils.py
@@ -46,14 +46,14 @@ def import_object(module_and_object):
     return getattr(module, object_name, None)
 
 
-def load_plugins(PLUGINS, INSTALLED_APPS, PLUGINS_CONFIG, VERSION, MIDDLEWARE, CACHEOPS):
+def load_plugins(settings):
     """Process plugins and log errors if they can't be loaded."""
-    for plugin_name in PLUGINS:
+    for plugin_name in settings.PLUGINS:
         # Attempt to load the plugin but let any errors bubble up.
-        load_plugin(plugin_name, INSTALLED_APPS, PLUGINS_CONFIG, VERSION, MIDDLEWARE, CACHEOPS)
+        load_plugin(plugin_name, settings)
 
 
-def load_plugin(plugin_name, INSTALLED_APPS, PLUGINS_CONFIG, VERSION, MIDDLEWARE, CACHEOPS):
+def load_plugin(plugin_name, settings):
     """Process a single plugin or raise errors that get bubbled up."""
 
     logger.debug(f"Loading {plugin_name}!")
@@ -80,23 +80,29 @@ def load_plugin(plugin_name, INSTALLED_APPS, PLUGINS_CONFIG, VERSION, MIDDLEWARE
 
     # Validate user-provided configuration settings and assign defaults. Plugin
     # validation that fails will stop before modifying any settings.
-    if plugin_name not in PLUGINS_CONFIG:
-        PLUGINS_CONFIG[plugin_name] = {}
-    plugin_config.validate(PLUGINS_CONFIG[plugin_name], VERSION)
+    if plugin_name not in settings.PLUGINS_CONFIG:
+        settings.PLUGINS_CONFIG[plugin_name] = {}
+    plugin_config.validate(settings.PLUGINS_CONFIG[plugin_name], settings.VERSION)
 
     # Plugin config is valid, so now we can and add to INSTALLED_APPS.
-    INSTALLED_APPS.append(f"{plugin_config.__module__}.{plugin_config.__name__}")
+    plugin_import_path = f"{plugin_config.__module__}.{plugin_config.__name__}"
+    if plugin_import_path not in settings.INSTALLED_APPS:
+        settings.INSTALLED_APPS.append(plugin_import_path)
 
     # Include any extra installed apps provided by the plugin
     # TODO(jathan): We won't be able to support advanced app-ordering concerns
     # and if the time comes that we do, this will have to be rethought.
-    INSTALLED_APPS.extend(plugin_config.installed_apps)
+    for plugin_installed_app in plugin_config.installed_apps:
+        if plugin_installed_app not in settings.INSTALLED_APPS:
+            settings.INSTALLED_APPS.append(plugin_installed_app)
 
     # Include any extra middleware provided by the plugin
-    MIDDLEWARE.extend(plugin_config.middleware)
+    for middleware in plugin_config.middleware:
+        if middleware not in settings.MIDDLEWARE:
+            settings.MIDDLEWARE.append(middleware)
 
     # Update caching configg
-    CACHEOPS.update({f"{plugin_name}.{key}": value for key, value in plugin_config.caching_config.items()})
+    settings.CACHEOPS.update({f"{plugin_name}.{key}": value for key, value in plugin_config.caching_config.items()})
 
 
 def get_sso_backend_name(social_auth_module):


Test patch:
diff --git a/nautobot/extras/tests/test_plugins.py b/nautobot/extras/tests/test_plugins.py
--- a/nautobot/extras/tests/test_plugins.py
+++ b/nautobot/extras/tests/test_plugins.py
@@ -310,25 +310,13 @@ def test_load_plugin(self):
         """Test `load_plugin`."""
 
         plugin_name = "bad.plugin"  # Start with a bogus plugin name
-        installed_apps = settings.INSTALLED_APPS
-        plugins_config = settings.PLUGINS_CONFIG
-        version = settings.VERSION
-        middleware = settings.MIDDLEWARE
-        cacheops = settings.CACHEOPS
 
         # FIXME(jathan): We're expecting a PluginNotFound to be raised, but
         # unittest doesn't appear to let that happen and we only see the
         # original ModuleNotFoundError, so this will have to do for now.
         with self.assertRaises(ModuleNotFoundError):
-            load_plugin(
-                plugin_name,
-                installed_apps,
-                plugins_config,
-                version,
-                middleware,
-                cacheops,
-            )
+            load_plugin(plugin_name, settings)
 
         # Move to the dummy plugin. No errors should be raised (which is good).
         plugin_name = "nautobot.extras.tests.dummy_plugin"
-        load_plugin(plugin_name, installed_apps, plugins_config, version, middleware, cacheops)
+        load_plugin(plugin_name, settings)