diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index ee7f59196..38fbc3b47 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -1,12 +1,13 @@ import collections +import importlib import inspect +import sys from packaging import version from django.apps import AppConfig from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.template.loader import get_template -from django.utils.module_loading import import_string from extras.registry import registry from utilities.choices import ButtonColorChoices @@ -60,18 +61,26 @@ class PluginConfig(AppConfig): def ready(self): # Register template content - try: - template_extensions = import_string(f"{self.__module__}.{self.template_extensions}") - register_template_extensions(template_extensions) - except ImportError: - pass + module, attr = f"{self.__module__}.{self.template_extensions}".rsplit('.', 1) + spec = importlib.util.find_spec(module) + if spec is not None: + template_content = importlib.util.module_from_spec(spec) + sys.modules[module] = template_content + spec.loader.exec_module(template_content) + if hasattr(template_content, attr): + template_extensions = getattr(template_content, attr) + register_template_extensions(template_extensions) # Register navigation menu items (if defined) - try: - menu_items = import_string(f"{self.__module__}.{self.menu_items}") - register_menu_items(self.verbose_name, menu_items) - except ImportError: - pass + module, attr = f"{self.__module__}.{self.menu_items}".rsplit('.', 1) + spec = importlib.util.find_spec(module) + if spec is not None: + navigation = importlib.util.module_from_spec(spec) + sys.modules[module] = navigation + spec.loader.exec_module(navigation) + if hasattr(navigation, attr): + menu_items = getattr(navigation, attr) + register_menu_items(self.verbose_name, menu_items) @classmethod def validate(cls, user_config): diff --git a/netbox/extras/plugins/urls.py b/netbox/extras/plugins/urls.py index b4360dc9e..d5925f1c8 100644 --- a/netbox/extras/plugins/urls.py +++ b/netbox/extras/plugins/urls.py @@ -1,9 +1,11 @@ +import importlib +import sys + from django.apps import apps from django.conf import settings from django.conf.urls import include from django.contrib.admin.views.decorators import staff_member_required from django.urls import path -from django.utils.module_loading import import_string from . import views @@ -24,19 +26,29 @@ for plugin_path in settings.PLUGINS: base_url = getattr(app, 'base_url') or app.label # Check if the plugin specifies any base URLs - try: - urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns") - plugin_patterns.append( - path(f"{base_url}/", include((urlpatterns, app.label))) - ) - except ImportError: - pass + spec = importlib.util.find_spec(f"{plugin_path}.urls") + if spec is not None: + # The plugin has a .urls module - import it + urls = importlib.util.module_from_spec(spec) + sys.modules[f"{plugin_path}.urls"] = urls + spec.loader.exec_module(urls) + if hasattr(urls, "urlpatterns"): + urlpatterns = urls.urlpatterns + plugin_patterns.append( + path(f"{base_url}/", include((urlpatterns, app.label))) + ) # Check if the plugin specifies any API URLs - try: - urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns") - plugin_api_patterns.append( - path(f"{base_url}/", include((urlpatterns, f"{app.label}-api"))) - ) - except ImportError: - pass + spec = importlib.util.find_spec(f"{plugin_path}.api") + if spec is not None: + spec = importlib.util.find_spec(f"{plugin_path}.api.urls") + if spec is not None: + # The plugin has a .api.urls module - import it + api_urls = importlib.util.module_from_spec(spec) + sys.modules[f"{plugin_path}.api.urls"] = api_urls + spec.loader.exec_module(api_urls) + if hasattr(api_urls, "urlpatterns"): + urlpatterns = api_urls.urlpatterns + plugin_api_patterns.append( + path(f"{base_url}/", include((urlpatterns, f"{app.label}-api"))) + ) diff --git a/netbox/extras/plugins/views.py b/netbox/extras/plugins/views.py index 39aa692d7..cbbf28791 100644 --- a/netbox/extras/plugins/views.py +++ b/netbox/extras/plugins/views.py @@ -1,10 +1,11 @@ from collections import OrderedDict +import importlib +import sys from django.apps import apps from django.conf import settings from django.shortcuts import render from django.urls.exceptions import NoReverseMatch -from django.utils.module_loading import import_string from django.views.generic import View from rest_framework import permissions from rest_framework.response import Response @@ -60,11 +61,23 @@ class PluginsAPIRootView(APIView): @staticmethod def _get_plugin_entry(plugin, app_config, request, format): - try: - api_app_name = import_string(f"{plugin}.api.urls.app_name") - except (ImportError, ModuleNotFoundError): - # Plugin does not expose an API + # Check if the plugin specifies any API URLs + spec = importlib.util.find_spec(f"{plugin}.api") + if spec is None: + # There is no plugin.api module return None + spec = importlib.util.find_spec(f"{plugin}.api.urls") + if spec is None: + # There is no plugin.api.urls module + return None + # The plugin has a .api.urls module - import it + api_urls = importlib.util.module_from_spec(spec) + sys.modules[f"{plugin}.api.urls"] = api_urls + spec.loader.exec_module(api_urls) + if not hasattr(api_urls, "app_name"): + # The plugin api.urls does not declare an app_name string + return None + api_app_name = api_urls.app_name try: entry = (getattr(app_config, 'base_url', app_config.label), reverse( @@ -73,7 +86,7 @@ class PluginsAPIRootView(APIView): format=format )) except NoReverseMatch: - # The plugin does not include an api-root + # The plugin does not include an api-root url entry = None return entry