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
|
# 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
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_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')
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
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