diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index 12e79d2dc..bab798f2f 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -19,6 +19,7 @@ class PluginConfig(AppConfig): """ # Plugin metadata author = '' + author_email = '' description = '' version = '' @@ -182,7 +183,7 @@ def register_nav_menu_links(): default_app_config = getattr(module, 'default_app_config') module, app_config = default_app_config.rsplit('.', 1) app_config = getattr(importlib.import_module(module), app_config) - section_name = app_config.NetBoxPluginMeta.name + section_name = getattr(app_config, 'verbose_name', app_config.name) if not isinstance(response, list): response = [response] diff --git a/netbox/extras/plugins/urls.py b/netbox/extras/plugins/urls.py new file mode 100644 index 000000000..1dc36befb --- /dev/null +++ b/netbox/extras/plugins/urls.py @@ -0,0 +1,53 @@ +import importlib + +from django.apps import apps +from django.conf import settings +from django.conf.urls import include +from django.core.exceptions import ImproperlyConfigured +from django.urls import path +from django.utils.module_loading import import_string + +from . import views + +# Plugins +plugin_patterns = [] +plugin_api_patterns = [] + +for plugin in settings.PLUGINS: + app = apps.get_app_config(plugin) + + url_slug = getattr(app, 'url_slug') or app.label + + # Check if the plugin specifies any URLs + try: + urlpatterns = import_string(f"{plugin}.urls.urlpatterns") + except ImportError: + # No urls defined + urlpatterns = None + if urlpatterns: + plugin_patterns.append( + path(f"{url_slug}/", include((urlpatterns, app.label))) + ) + + # Check if the plugin specifies any API URLs + try: + urlpatterns = import_string(f"{plugin}.api.urls.urlpatterns") + app_name = import_string(f"{plugin}.api.urls.app_name") + except ImportError: + # No urls defined + urlpatterns = None + if urlpatterns: + plugin_api_patterns.append( + path(f"{url_slug}/", include((urlpatterns, app_name))) + ) + +# Plugin list admin view +admin_plugin_patterns = [ + path('', views.installed_plugins_admin_view, name='plugins_list') +] + +# Plugin list API view +plugin_api_patterns += [ + path('', views.PluginsAPIRootView.as_view(), name='api-root'), + path('installed-plugins/', views.InstalledPluginsAPIView.as_view(), name='plugins-list') +] diff --git a/netbox/extras/plugins/views.py b/netbox/extras/plugins/views.py new file mode 100644 index 000000000..690a708b6 --- /dev/null +++ b/netbox/extras/plugins/views.py @@ -0,0 +1,95 @@ +from collections import OrderedDict + +from django.apps import apps +from django.conf import settings +from django.contrib import admin +from django.contrib.admin.views.decorators import staff_member_required +from django.urls.exceptions import NoReverseMatch +from django.utils.module_loading import import_string +from django.shortcuts import render +from django.views.generic import View +from rest_framework import authentication, permissions, routers +from rest_framework.response import Response +from rest_framework.reverse import reverse +from rest_framework.views import APIView + + +@staff_member_required +def installed_plugins_admin_view(request): + """ + Admin view for listing all installed plugins + """ + context_data = { + 'plugins': [apps.get_app_config(plugin) for plugin in settings.PLUGINS] + } + return render(request, 'extras/admin/plugins_list.html', context_data) + + +class InstalledPluginsAPIView(APIView): + """ + API view for listing all installed plugins + """ + permission_classes = [permissions.IsAdminUser] + _ignore_model_permissions = True + exclude_from_schema = True + swagger_schema = None + + def get_view_name(self): + return "Installed Plugins" + + @staticmethod + def _get_plugin_data(plugin_app_config): + return { + 'name': plugin_app_config.verbose_name, + 'package': plugin_app_config.name, + 'author': plugin_app_config.author, + 'author_email': plugin_app_config.author_email, + 'description': plugin_app_config.description, + 'verison': plugin_app_config.version + } + + def get(self, request, format=None): + return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS]) + + +class PluginsAPIRootView(APIView): + _ignore_model_permissions = True + exclude_from_schema = True + swagger_schema = None + + def get_view_name(self): + return "Plugins" + + @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 + return None + + try: + entry = (getattr(app_config, 'url_slug', app_config.label), reverse( + f"plugins-api:{api_app_name}:api-root", + request=request, + format=format + )) + except NoReverseMatch: + # The plugin does not include an api-root + entry = None + + return entry + + def get(self, request, format=None): + + entries = [] + for plugin in settings.PLUGINS: + app_config = apps.get_app_config(plugin) + entry = self._get_plugin_entry(plugin, app_config, request, format) + if entry is not None: + entries.append(entry) + + return Response(OrderedDict(( + ('installed-plugins', reverse('plugins-api:plugins-list', request=request, format=format)), + *entries + ))) diff --git a/netbox/netbox/admin.py b/netbox/netbox/admin.py index 222c6ffc5..63fcac550 100644 --- a/netbox/netbox/admin.py +++ b/netbox/netbox/admin.py @@ -11,7 +11,7 @@ class NetBoxAdminSite(AdminSite): site_header = 'NetBox Administration' site_title = 'NetBox' site_url = '/{}'.format(settings.BASE_PATH) - index_template = 'django_rq/index.html' + index_template = 'admin/index.html' admin_site = NetBoxAdminSite(name='admin') diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 6fed9dafe..c86a088cc 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -638,13 +638,13 @@ if PAGINATE_COUNT not in PER_PAGE_DEFAULTS: # Plugins # -PLUGINS = [] +PLUGINS = set() if PLUGINS_ENABLED: for entry_point in iter_entry_points(group='netbox_plugins', name=None): plugin = entry_point.module_name app_config = entry_point.load() - PLUGINS.append(plugin) + PLUGINS.add(plugin) INSTALLED_APPS.append(plugin) # Check version constraints @@ -688,4 +688,7 @@ if PLUGINS_ENABLED: raise ImproperlyConfigured(f"Plugin {plugin} caching_config is invalid!") if app != plugin: raise ImproperlyConfigured(f"Plugin {plugin} may not modify caching config for another app!") + else: + # Apply the default config like all other core apps + plugin_cacheops = {f"{plugin}.*": {'ops': 'all'}} CACHEOPS.update(plugin_cacheops) diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 657e3d6b4..4a535ad31 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -8,7 +8,7 @@ from django.views.static import serve from drf_yasg import openapi from drf_yasg.views import get_schema_view -from extras.plugins import PluginConfig +from extras.plugins.urls import admin_plugin_patterns, plugin_patterns, plugin_api_patterns from netbox.views import APIRootView, HomeView, StaticMediaFailureView, SearchView from users.views import LoginView, LogoutView from .admin import admin_site @@ -70,42 +70,12 @@ _patterns = [ # Errors path('media-failure/', StaticMediaFailureView.as_view(), name='media_failure'), + # Plugins + path('plugins/', include((plugin_patterns, 'plugins'))), + path('api/plugins/', include((plugin_api_patterns, 'plugins-api'))), + path('admin/plugins/installed-plugins/', include(admin_plugin_patterns)) ] -# Plugins -plugin_patterns = [] -plugin_api_patterns = [] -for app in apps.get_app_configs(): - # Loop over all apps look for installed plugins - if isinstance(app, PluginConfig): - # Check if the plugin specifies any URLs - if importlib.util.find_spec('{}.urls'.format(app.name)): - urls = importlib.import_module('{}.urls'.format(app.name)) - url_slug = getattr(app, 'url_slug') or app.label - if hasattr(urls, 'urlpatterns'): - # Mount URLs at `/` - plugin_patterns.append( - path('{}/'.format(url_slug), include((urls.urlpatterns, app.label))) - ) - # Check if the plugin specifies any API URLs - if importlib.util.find_spec('{}.api'.format(app.name)): - if importlib.util.find_spec('{}.api.urls'.format(app.name)): - urls = importlib.import_module('{}.api.urls'.format(app.name)) - if hasattr(urls, 'urlpatterns'): - url_slug = getattr(app, 'url_slug') or app.label - # Mount URLs at `/` - plugin_api_patterns.append( - path('{}/'.format(url_slug), include((urls.urlpatterns, app.label))) - ) - -# Mount all plugin URLs within the `plugins` namespace -_patterns.append( - path('plugins/', include((plugin_patterns, 'plugins'))) -) -# Mount all plugin API URLs within the `plugins-api` namespace -_patterns.append( - path('api/plugins/', include((plugin_api_patterns, 'plugins-api'))) -) if settings.DEBUG: import debug_toolbar diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index bc87a825b..25c32338b 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -341,6 +341,7 @@ class APIRootView(APIView): ('dcim', reverse('dcim-api:api-root', request=request, format=format)), ('extras', reverse('extras-api:api-root', request=request, format=format)), ('ipam', reverse('ipam-api:api-root', request=request, format=format)), + ('plugins', reverse('plugins-api:api-root', request=request, format=format)), ('secrets', reverse('secrets-api:api-root', request=request, format=format)), ('tenancy', reverse('tenancy-api:api-root', request=request, format=format)), ('virtualization', reverse('virtualization-api:api-root', request=request, format=format)), diff --git a/netbox/templates/admin/index.html b/netbox/templates/admin/index.html new file mode 100644 index 000000000..00f720738 --- /dev/null +++ b/netbox/templates/admin/index.html @@ -0,0 +1,6 @@ +{% extends "django_rq/index.html" %} + +{% block sidebar %} + {{ block.super }} + {% include 'extras/admin/plugins_index.html' %} +{% endblock %} diff --git a/netbox/templates/extras/admin/plugins_index.html b/netbox/templates/extras/admin/plugins_index.html new file mode 100644 index 000000000..d34d55c23 --- /dev/null +++ b/netbox/templates/extras/admin/plugins_index.html @@ -0,0 +1,14 @@ +
+
+ + + + + + + +
Plugins
+ Installed plugins +
+
+
diff --git a/netbox/templates/extras/admin/plugins_list.html b/netbox/templates/extras/admin/plugins_list.html new file mode 100644 index 000000000..ecc003ccc --- /dev/null +++ b/netbox/templates/extras/admin/plugins_list.html @@ -0,0 +1,60 @@ +{% extends "admin/base_site.html" %} + +{% block title %}Installed Plugins {{ block.super }}{% endblock %} + + +{% block breadcrumbs %} + +{% endblock %} + +{% block content_title %}

Installed Plugins{{ queue.name }}

{% endblock %} + +{% block content %} + +
+
+
+ + + + + + + + + + + + + {% for plugin in plugins %} + + + + + + + + + {% endfor %} + +
Name
Package Name
Author
Author Email
Description
Version
+ {{ plugin.verbose_name }} + + {{ plugin.name }} + + {{ plugin.author }} + + {{ plugin.author_email }} + + {{ plugin.description }} + + {{ plugin.version }} +
+
+
+
+ +{% endblock %} \ No newline at end of file