Forráskód Böngészése

Closes #15915: Replace plugins list with an overall system status view (#15950)

* Replace plugins list with an overall system status view

* Enable export of system status data
Jeremy Stretch 1 éve
szülő
commit
8e1c2ecd92

+ 1 - 1
netbox/core/tables/plugins.py

@@ -35,5 +35,5 @@ class PluginTable(BaseTable):
             'name', 'version', 'package', 'author', 'author_email', 'description',
         )
         default_columns = (
-            'name', 'version', 'package', 'author', 'author_email', 'description',
+            'name', 'version', 'package', 'description',
         )

+ 2 - 5
netbox/core/urls.py

@@ -43,9 +43,6 @@ urlpatterns = (
     path('config-revisions/<int:pk>/restore/', views.ConfigRevisionRestoreView.as_view(), name='configrevision_restore'),
     path('config-revisions/<int:pk>/', include(get_model_urls('core', 'configrevision'))),
 
-    # Configuration
-    path('config/', views.ConfigView.as_view(), name='config'),
-
-    # Plugins
-    path('plugins/', views.PluginListView.as_view(), name='plugin_list'),
+    # System
+    path('system/', views.SystemView.as_view(), name='system'),
 )

+ 62 - 23
netbox/core/views.py

@@ -1,14 +1,19 @@
+import json
+import platform
+
+from django import __version__ as DJANGO_VERSION
 from django.apps import apps
 from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth.mixins import UserPassesTestMixin
 from django.core.cache import cache
-from django.http import HttpResponseForbidden, Http404
+from django.db import connection, ProgrammingError
+from django.http import HttpResponse, HttpResponseForbidden, Http404
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import View
-from django_rq.queues import get_queue_by_index, get_redis_connection
+from django_rq.queues import get_connection, get_queue_by_index, get_redis_connection
 from django_rq.settings import QUEUES_MAP, QUEUES_LIST
 from django_rq.utils import get_jobs, get_statistics, stop_jobs
 from rq import requeue_job
@@ -175,20 +180,6 @@ class JobBulkDeleteView(generic.BulkDeleteView):
 # Config Revisions
 #
 
-class ConfigView(generic.ObjectView):
-    queryset = ConfigRevision.objects.all()
-
-    def get_object(self, **kwargs):
-        revision_id = cache.get('config_version')
-        try:
-            return ConfigRevision.objects.get(pk=revision_id)
-        except ConfigRevision.DoesNotExist:
-            # Fall back to using the active config data if no record is found
-            return ConfigRevision(
-                data=get_config().defaults
-            )
-
-
 class ConfigRevisionListView(generic.ObjectListView):
     queryset = ConfigRevision.objects.all()
     filterset = filtersets.ConfigRevisionFilterSet
@@ -527,21 +518,69 @@ class WorkerView(BaseRQView):
 # Plugins
 #
 
-class PluginListView(UserPassesTestMixin, View):
+class SystemView(UserPassesTestMixin, View):
 
     def test_func(self):
         return self.request.user.is_staff
 
     def get(self, request):
+
+        # System stats
+        psql_version = db_name = db_size = None
+        try:
+            with connection.cursor() as cursor:
+                cursor.execute("SELECT version()")
+                psql_version = cursor.fetchone()[0]
+                psql_version = psql_version.split('(')[0].strip()
+                cursor.execute("SELECT current_database()")
+                db_name = cursor.fetchone()[0]
+                cursor.execute(f"SELECT pg_size_pretty(pg_database_size('{db_name}'))")
+                db_size = cursor.fetchone()[0]
+        except (ProgrammingError, IndexError):
+            pass
+        stats = {
+            'netbox_version': settings.VERSION,
+            'django_version': DJANGO_VERSION,
+            'python_version': platform.python_version(),
+            'postgresql_version': psql_version,
+            'database_name': db_name,
+            'database_size': db_size,
+            'rq_worker_count': Worker.count(get_connection('default')),
+        }
+
+        # Plugins
         plugins = [
             # Look up app config by package name
             apps.get_app_config(plugin.rsplit('.', 1)[-1]) for plugin in settings.PLUGINS
         ]
-        table = tables.PluginTable(plugins, user=request.user)
-        table.configure(request)
 
-        return render(request, 'core/plugin_list.html', {
-            'plugins': plugins,
-            'active_tab': 'api-tokens',
-            'table': table,
+        # Configuration
+        try:
+            config = ConfigRevision.objects.get(pk=cache.get('config_version'))
+        except ConfigRevision.DoesNotExist:
+            # Fall back to using the active config data if no record is found
+            config = ConfigRevision(data=get_config().defaults)
+
+        # Raw data export
+        if 'export' in request.GET:
+            data = {
+                **stats,
+                'plugins': {
+                    plugin.name: plugin.version for plugin in plugins
+                },
+                'config': {
+                    k: config.data[k] for k in sorted(config.data)
+                },
+            }
+            response = HttpResponse(json.dumps(data, indent=4), content_type='text/json')
+            response['Content-Disposition'] = 'attachment; filename="netbox.json"'
+            return response
+
+        plugins_table = tables.PluginTable(plugins, orderable=False)
+        plugins_table.configure(request)
+
+        return render(request, 'core/system.html', {
+            'stats': stats,
+            'plugins_table': plugins_table,
+            'config': config,
         })

+ 4 - 14
netbox/netbox/navigation/menu.py

@@ -421,27 +421,17 @@ ADMIN_MENU = Menu(
             ),
         ),
         MenuGroup(
-            label=_('Configuration'),
+            label=_('System'),
             items=(
                 MenuItem(
-                    link='core:config',
-                    link_text=_('Current Config'),
-                    permissions=['core.view_configrevision']
+                    link='core:system',
+                    link_text=_('System')
                 ),
                 MenuItem(
                     link='core:configrevision_list',
-                    link_text=_('Config Revisions'),
+                    link_text=_('Configuration History'),
                     permissions=['core.view_configrevision']
                 ),
-            ),
-        ),
-        MenuGroup(
-            label=_('System'),
-            items=(
-                MenuItem(
-                    link='core:plugin_list',
-                    link_text=_('Plugins')
-                ),
                 MenuItem(
                     link='core:background_queue_list',
                     link_text=_('Background Tasks')

+ 2 - 157
netbox/templates/core/configrevision.html

@@ -32,163 +32,8 @@
   <div class="row">
     <div class="col col-md-12">
       <div class="card">
-        <h5 class="card-header">{% trans "Rack Elevations" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Default unit height" %}</th>
-            <td>{{ object.data.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Default unit width" %}</th>
-            <td>{{ object.data.RACK_ELEVATION_DEFAULT_UNIT_WIDTH }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Power Feeds" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Default voltage" %}</th>
-            <td>{{ object.data.POWERFEED_DEFAULT_VOLTAGE }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Default amperage" %}</th>
-            <td>{{ object.data.POWERFEED_DEFAULT_AMPERAGE }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Default max utilization" %}</th>
-            <td>{{ object.data.POWERFEED_DEFAULT_MAX_UTILIZATION }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "IPAM" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Enforce global unique" %}</th>
-            <td>{{ object.data.ENFORCE_GLOBAL_UNIQUE }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Prefer IPv4" %}</th>
-            <td>{{ object.data.PREFER_IPV4 }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Security" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Allowed URL schemes" %}</th>
-            <td>{{ object.data.ALLOWED_URL_SCHEMES|join:", "|placeholder }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Banners" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Login banner" %}</th>
-            <td>{{ object.data.BANNER_LOGIN }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Maintenance banner" %}</th>
-            <td>{{ object.data.BANNER_MAINTENANCE }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Top banner" %}</th>
-            <td>{{ object.data.BANNER_TOP }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Bottom banner" %}</th>
-            <td>{{ object.data.BANNER_BOTTOM }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Pagination" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Paginate count" %}</th>
-            <td>{{ object.data.PAGINATE_COUNT }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Max page size" %}</th>
-            <td>{{ object.data.MAX_PAGE_SIZE }}</td>
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Validation" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Custom validators" %}</th>
-            {% if object.data.CUSTOM_VALIDATORS %}
-              <td class="font-monospace">
-                <pre>{{ object.data.CUSTOM_VALIDATORS|json }}</pre>
-              </td>
-            {% else %}
-              <td>{{ ''|placeholder }}</td>
-            {% endif %}
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Protection rules" %}</th>
-            {% if object.data.PROTECTION_RULES %}
-              <td>
-                <pre>{{ object.data.PROTECTION_RULES|json }}</pre>
-              </td>
-            {% else %}
-              <td>{{ ''|placeholder }}</td>
-            {% endif %}
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "User Preferences" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Default user preferences" %}</th>
-            {% if object.data.DEFAULT_USER_PREFERENCES %}
-              <td>
-                <pre>{{ object.data.DEFAULT_USER_PREFERENCES|json }}</pre>
-              </td>
-            {% else %}
-              <td>{{ ''|placeholder }}</td>
-            {% endif %}
-          </tr>
-        </table>
-      </div>
-
-      <div class="card">
-        <h5 class="card-header">{% trans "Miscellaneous" %}</h5>
-        <table class="table table-hover attr-table">
-          <tr>
-            <th scope="row">{% trans "Maintenance mode" %}</th>
-            <td>{{ object.data.MAINTENANCE_MODE }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "GraphQL enabled" %}</th>
-            <td>{{ object.data.GRAPHQL_ENABLED }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Changelog retention" %}</th>
-            <td>{{ object.data.CHANGELOG_RETENTION }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Job retention" %}</th>
-            <td>{{ object.data.JOB_RETENTION }}</td>
-          </tr>
-          <tr>
-            <th scope="row">{% trans "Maps URL" %}</th>
-            <td>{{ object.data.MAPS_URL }}</td>
-          </tr>
-        </table>
+        <h5 class="card-header">{% trans "Configuration Data" %}</h5>
+        {% include 'core/inc/config_data.html' with config=config.data %}
       </div>
 
       <div class="card">

+ 149 - 0
netbox/templates/core/inc/config_data.html

@@ -0,0 +1,149 @@
+{% load i18n %}
+
+<table class="table table-hover attr-table">
+
+  {# Rack elevations #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Rack elevations" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Default unit height" %}</th>
+    <td>{{ config.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Default unit width" %}</th>
+    <td class="border-0">{{ config.RACK_ELEVATION_DEFAULT_UNIT_WIDTH }}</td>
+  </tr>
+
+  {# Power feeds #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Power feeds" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Default voltage" %}</th>
+    <td>{{ config.POWERFEED_DEFAULT_VOLTAGE }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Default amperage" %}</th>
+    <td>{{ config.POWERFEED_DEFAULT_AMPERAGE }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Default max utilization" %}</th>
+    <td class="border-0">{{ config.POWERFEED_DEFAULT_MAX_UTILIZATION }}</td>
+  </tr>
+
+  {# IPAM #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "IPAM" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Enforce global unique" %}</th>
+    <td>{% checkmark config.ENFORCE_GLOBAL_UNIQUE %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Prefer IPv4" %}</th>
+    <td class="border-0">{% checkmark config.PREFER_IPV4 %}</td>
+  </tr>
+
+  {# Security #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Security" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Allowed URL schemes" %}</th>
+    <td class="border-0">{{ config.ALLOWED_URL_SCHEMES|join:", "|placeholder }}</td>
+  </tr>
+
+  {# Banners #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Banners" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Login banner" %}</th>
+    <td>{{ config.BANNER_LOGIN|placeholder }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Maintenance banner" %}</th>
+    <td>{{ config.BANNER_MAINTENANCE|placeholder }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Top banner" %}</th>
+    <td>{{ config.BANNER_TOP|placeholder }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Bottom banner" %}</th>
+    <td class="border-0">{{ config.BANNER_BOTTOM|placeholder }}</td>
+  </tr>
+
+  {# Pagination #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Pagination" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Paginate count" %}</th>
+    <td>{{ config.PAGINATE_COUNT }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Max page size" %}</th>
+    <td class="border-0">{{ config.MAX_PAGE_SIZE }}</td>
+  </tr>
+
+  {# Validation #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Validation" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Custom validators" %}</th>
+    {% if config.CUSTOM_VALIDATORS %}
+      <td><pre>{{ config.CUSTOM_VALIDATORS|json }}</pre></td>
+    {% else %}
+      <td>{{ ''|placeholder }}</td>
+    {% endif %}
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Protection rules" %}</th>
+    {% if config.PROTECTION_RULES %}
+      <td class="border-0"><pre>{{ config.PROTECTION_RULES|json }}</pre></td>
+    {% else %}
+      <td class="border-0">{{ ''|placeholder }}</td>
+    {% endif %}
+  </tr>
+
+  {# User Preferences #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "User preferences" %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Default preferences" %}</th>
+    {% if config.DEFAULT_USER_PREFERENCES %}
+      <td class="border-0"><pre>{{ config.DEFAULT_USER_PREFERENCES|json }}</pre></td>
+    {% else %}
+      <td class="border-0">{{ ''|placeholder }}</td>
+    {% endif %}
+  </tr>
+
+  {# Miscellaneous #}
+  <tr>
+    <td colspan="2" class="bg-secondary-subtle fs-5 fw-bold border-0 py-1">{% trans "Miscellaneous" %}</td>
+  <tr>
+  <th scope="row" class="ps-3">{% trans "Maintenance mode" %}</th>
+    <td>{% checkmark config.MAINTENANCE_MODE %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "GraphQL enabled" %}</th>
+    <td>{% checkmark config.GRAPHQL_ENABLED %}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Changelog retention" %}</th>
+    <td>{{ config.CHANGELOG_RETENTION }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="ps-3">{% trans "Job retention" %}</th>
+    <td>{{ config.JOB_RETENTION }}</td>
+  </tr>
+  <tr>
+    <th scope="row" class="border-0 ps-3">{% trans "Maps URL" %}</th>
+    <td class="border-0">{{ config.MAPS_URL }}</td>
+  </tr>
+
+</table>

+ 0 - 36
netbox/templates/core/plugin_list.html

@@ -1,36 +0,0 @@
-{% extends 'generic/_base.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load i18n %}
-{% load render_table from django_tables2 %}
-
-{% block title %}{% trans "Plugins" %}{% endblock %}
-
-{% block tabs %}
-  <ul class="nav nav-tabs px-3">
-    <li class="nav-item" role="presentation">
-      <a class="nav-link active" role="tab">{% trans "Plugins" %}</a>
-    </li>
-  </ul>
-{% endblock tabs %}
-
-{% block content %}
-  <div class="row mb-3">
-    <div class="col-auto ms-auto d-print-none">
-      {# Table configuration button #}
-      <div class="table-configure input-group">
-        <button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#ObjectTable_config" class="btn">
-          <i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
-        </button>
-      </div>
-    </div>
-  </div>
-
-  <div class="card">
-    {% render_table table %}
-  </div>
-{% endblock content %}
-
-{% block modals %}
-  {% table_config_form table table_name="ObjectTable" %}
-{% endblock modals %}

+ 92 - 0
netbox/templates/core/system.html

@@ -0,0 +1,92 @@
+{% extends 'generic/_base.html' %}
+{% load buttons %}
+{% load helpers %}
+{% load i18n %}
+{% load render_table from django_tables2 %}
+
+{% block title %}{% trans "System" %}{% endblock %}
+
+{% block controls %}
+  <a href="?export=true" class="btn btn-purple">
+    <i class="mdi mdi-download"></i> {% trans "Export" %}
+  </a>
+{% endblock controls %}
+
+{% block tabs %}
+  <ul class="nav nav-tabs px-3">
+    <li class="nav-item" role="presentation">
+      <a class="nav-link active" role="tab">{% trans "Status" %}</a>
+    </li>
+  </ul>
+{% endblock tabs %}
+
+{% block content %}
+  {# System status #}
+  <div class="row mb-3">
+    <div class="col">
+      <div class="card">
+        <h5 class="card-header">{% trans "System Status" %}</h5>
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">{% trans "NetBox version" %}</th>
+            <td>{{ stats.netbox_version }}</td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "Django version" %}</th>
+            <td>{{ stats.django_version }}</td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "PotsgreSQL version" %}</th>
+            <td>{{ stats.postgresql_version }}</td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "Database name" %}</th>
+            <td>{{ stats.database_name }}</td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "Database size" %}</th>
+            <td>
+              {% if stats.database_size %}
+                {{ stats.database_size }}
+              {% else %}
+                <span class="text-muted">{% trans "Unavailable" %}</span>
+              {% endif %}
+            </td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "RQ workers" %}</th>
+            <td>
+              <a href="{% url 'core:background_queue_list' %}">{{ stats.rq_worker_count }}</a>
+              ({% trans "default queue" %})
+            </td>
+          </tr>
+          <tr>
+            <th scope="row">{% trans "System time" %}</th>
+            <td>{% now 'Y-m-d H:i:s T' %}</td>
+          </tr>
+        </table>
+      </div>
+    </div>
+  </div>
+
+  {# Plugins #}
+  <div class="row mb-3">
+    <div class="col col-md-12">
+      <div class="card">
+        <h5 class="card-header">{% trans "Plugins" %}</h5>
+        {% render_table plugins_table %}
+      </div>
+    </div>
+  </div>
+
+  {# Configuration #}
+  <div class="row mb-3">
+    <div class="col col-md-12">
+      <div class="card">
+        <h5 class="card-header">{% trans "Current Configuration" %}</h5>
+        {% include 'core/inc/config_data.html' with config=config.data %}
+      </div>
+
+    </div>
+  </div>
+{% endblock content %}