Просмотр исходного кода

added admin and api views for listing all plugins, and refactored urls import

John Anderson 5 лет назад
Родитель
Сommit
4e84e8048f

+ 2 - 1
netbox/extras/plugins/__init__.py

@@ -19,6 +19,7 @@ class PluginConfig(AppConfig):
     """
     """
     # Plugin metadata
     # Plugin metadata
     author = ''
     author = ''
+    author_email = ''
     description = ''
     description = ''
     version = ''
     version = ''
 
 
@@ -182,7 +183,7 @@ def register_nav_menu_links():
         default_app_config = getattr(module, 'default_app_config')
         default_app_config = getattr(module, 'default_app_config')
         module, app_config = default_app_config.rsplit('.', 1)
         module, app_config = default_app_config.rsplit('.', 1)
         app_config = getattr(importlib.import_module(module), app_config)
         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):
         if not isinstance(response, list):
             response = [response]
             response = [response]

+ 53 - 0
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')
+]

+ 95 - 0
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
+        )))

+ 1 - 1
netbox/netbox/admin.py

@@ -11,7 +11,7 @@ class NetBoxAdminSite(AdminSite):
     site_header = 'NetBox Administration'
     site_header = 'NetBox Administration'
     site_title = 'NetBox'
     site_title = 'NetBox'
     site_url = '/{}'.format(settings.BASE_PATH)
     site_url = '/{}'.format(settings.BASE_PATH)
-    index_template = 'django_rq/index.html'
+    index_template = 'admin/index.html'
 
 
 
 
 admin_site = NetBoxAdminSite(name='admin')
 admin_site = NetBoxAdminSite(name='admin')

+ 5 - 2
netbox/netbox/settings.py

@@ -638,13 +638,13 @@ if PAGINATE_COUNT not in PER_PAGE_DEFAULTS:
 # Plugins
 # Plugins
 #
 #
 
 
-PLUGINS = []
+PLUGINS = set()
 if PLUGINS_ENABLED:
 if PLUGINS_ENABLED:
     for entry_point in iter_entry_points(group='netbox_plugins', name=None):
     for entry_point in iter_entry_points(group='netbox_plugins', name=None):
         plugin = entry_point.module_name
         plugin = entry_point.module_name
         app_config = entry_point.load()
         app_config = entry_point.load()
 
 
-        PLUGINS.append(plugin)
+        PLUGINS.add(plugin)
         INSTALLED_APPS.append(plugin)
         INSTALLED_APPS.append(plugin)
 
 
         # Check version constraints
         # Check version constraints
@@ -688,4 +688,7 @@ if PLUGINS_ENABLED:
                     raise ImproperlyConfigured(f"Plugin {plugin} caching_config is invalid!")
                     raise ImproperlyConfigured(f"Plugin {plugin} caching_config is invalid!")
                 if app != plugin:
                 if app != plugin:
                     raise ImproperlyConfigured(f"Plugin {plugin} may not modify caching config for another app!")
                     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)
         CACHEOPS.update(plugin_cacheops)

+ 5 - 35
netbox/netbox/urls.py

@@ -8,7 +8,7 @@ from django.views.static import serve
 from drf_yasg import openapi
 from drf_yasg import openapi
 from drf_yasg.views import get_schema_view
 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 netbox.views import APIRootView, HomeView, StaticMediaFailureView, SearchView
 from users.views import LoginView, LogoutView
 from users.views import LoginView, LogoutView
 from .admin import admin_site
 from .admin import admin_site
@@ -70,42 +70,12 @@ _patterns = [
     # Errors
     # Errors
     path('media-failure/', StaticMediaFailureView.as_view(), name='media_failure'),
     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 `<url_slug>/<path>`
-                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 `<url_slug>/<path>`
-                    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:
 if settings.DEBUG:
     import debug_toolbar
     import debug_toolbar

+ 1 - 0
netbox/netbox/views.py

@@ -341,6 +341,7 @@ class APIRootView(APIView):
             ('dcim', reverse('dcim-api:api-root', request=request, format=format)),
             ('dcim', reverse('dcim-api:api-root', request=request, format=format)),
             ('extras', reverse('extras-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)),
             ('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)),
             ('secrets', reverse('secrets-api:api-root', request=request, format=format)),
             ('tenancy', reverse('tenancy-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)),
             ('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),

+ 6 - 0
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 %}

+ 14 - 0
netbox/templates/extras/admin/plugins_index.html

@@ -0,0 +1,14 @@
+<div id="django-rq">
+    <div class="module">
+        <table>
+            <caption>Plugins</caption>
+            <tbody>
+                <tr>
+                    <th>
+                        <a href = "{% url 'plugins_list' %}">Installed plugins</a>
+                    </th>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+</div>

+ 60 - 0
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 %}
+    <div class="breadcrumbs">
+        <a href="{% url 'admin:index' %}">Home</a> &rsaquo;
+        <a href="{% url 'plugins_list' %}">Installed Plugins</a>
+    </div>
+{% endblock %}
+
+{% block content_title %}<h1>Installed Plugins{{ queue.name }}</h1>{% endblock %}
+
+{% block content %}
+
+<div id="content-main">
+    <div class="module" id="changelist">
+        <div class="results">
+            <table id="result_list">
+                <thead>
+                    <tr>
+                        <th><div class = 'text'><span>Name</span></div></th>
+                        <th><div class = 'text'><span>Package Name</span></div></th>
+                        <th><div class = 'text'><span>Author</span></div></th>
+                        <th><div class = 'text'><span>Author Email</span></div></th>
+                        <th><div class = 'text'><span>Description</span></div></th>
+                        <th><div class = 'text'><span>Version</span></div></th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {% for plugin in plugins %}
+                        <tr class = "{% cycle 'row1' 'row2' %}">
+                            <td>
+                                {{ plugin.verbose_name }}
+                            </td>
+                            <td>
+                                {{ plugin.name }}
+                            </td>
+                            <td>
+                                {{ plugin.author }}
+                            </td>
+                            <td>
+                                {{ plugin.author_email }}
+                            </td>
+                            <td>
+                                {{ plugin.description }}
+                            </td>
+                            <td>
+                                {{ plugin.version }}
+                            </td>
+                        </tr>
+                    {% endfor %}
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+
+{% endblock %}