mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
added admin and api views for listing all plugins, and refactored urls import
This commit is contained in:
@ -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]
|
||||
|
53
netbox/extras/plugins/urls.py
Normal file
53
netbox/extras/plugins/urls.py
Normal file
@ -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
netbox/extras/plugins/views.py
Normal file
95
netbox/extras/plugins/views.py
Normal file
@ -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
|
||||
)))
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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 `<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:
|
||||
import debug_toolbar
|
||||
|
@ -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)),
|
||||
|
6
netbox/templates/admin/index.html
Normal file
6
netbox/templates/admin/index.html
Normal file
@ -0,0 +1,6 @@
|
||||
{% extends "django_rq/index.html" %}
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
{% include 'extras/admin/plugins_index.html' %}
|
||||
{% endblock %}
|
14
netbox/templates/extras/admin/plugins_index.html
Normal file
14
netbox/templates/extras/admin/plugins_index.html
Normal file
@ -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
netbox/templates/extras/admin/plugins_list.html
Normal file
60
netbox/templates/extras/admin/plugins_list.html
Normal file
@ -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> ›
|
||||
<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 %}
|
Reference in New Issue
Block a user