|
|
@@ -3,11 +3,12 @@ import platform
|
|
|
from copy import deepcopy
|
|
|
|
|
|
from django import __version__ as django_version
|
|
|
+from django.apps import apps as django_apps_registry
|
|
|
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.db import ProgrammingError, connection
|
|
|
+from django.db import DatabaseError, connection
|
|
|
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
|
from django.urls import reverse
|
|
|
@@ -22,10 +23,18 @@ from rq.job import JobStatus as RQJobStatus
|
|
|
from rq.worker import Worker
|
|
|
from rq.worker_registration import clean_worker_registry
|
|
|
|
|
|
-from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
|
|
|
+from core.utils import (
|
|
|
+ delete_rq_job,
|
|
|
+ enqueue_rq_job,
|
|
|
+ get_db_schema,
|
|
|
+ get_rq_jobs_from_status,
|
|
|
+ requeue_rq_job,
|
|
|
+ stop_rq_job,
|
|
|
+)
|
|
|
from extras.ui.panels import CustomFieldsPanel, TagsPanel
|
|
|
from netbox.config import PARAMS, get_config
|
|
|
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
|
|
|
+from netbox.plugins import PluginConfig
|
|
|
from netbox.plugins.utils import get_installed_plugins
|
|
|
from netbox.ui import layout
|
|
|
from netbox.ui.panels import (
|
|
|
@@ -674,14 +683,13 @@ class WorkerView(BaseRQView):
|
|
|
# System
|
|
|
#
|
|
|
|
|
|
+
|
|
|
class SystemView(UserPassesTestMixin, View):
|
|
|
|
|
|
def test_func(self):
|
|
|
return self.request.user.is_superuser
|
|
|
|
|
|
- def get(self, request):
|
|
|
-
|
|
|
- # System status
|
|
|
+ def _get_stats(self):
|
|
|
psql_version = db_name = db_size = None
|
|
|
try:
|
|
|
with connection.cursor() as cursor:
|
|
|
@@ -690,11 +698,11 @@ class SystemView(UserPassesTestMixin, View):
|
|
|
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}'))")
|
|
|
+ cursor.execute("SELECT pg_size_pretty(pg_database_size(current_database()))")
|
|
|
db_size = cursor.fetchone()[0]
|
|
|
- except (ProgrammingError, IndexError):
|
|
|
+ except (DatabaseError, IndexError):
|
|
|
pass
|
|
|
- stats = {
|
|
|
+ return {
|
|
|
'netbox_release': settings.RELEASE,
|
|
|
'django_version': django_version,
|
|
|
'python_version': platform.python_version(),
|
|
|
@@ -704,23 +712,23 @@ class SystemView(UserPassesTestMixin, View):
|
|
|
'rq_worker_count': Worker.count(get_connection('default')),
|
|
|
}
|
|
|
|
|
|
- # Django apps
|
|
|
- django_apps = get_installed_apps()
|
|
|
-
|
|
|
- # Configuration
|
|
|
- config = get_config()
|
|
|
-
|
|
|
- # Plugins
|
|
|
- plugins = get_installed_plugins()
|
|
|
-
|
|
|
- # Object counts
|
|
|
+ def _get_object_counts(self):
|
|
|
objects = {}
|
|
|
for ot in ObjectType.objects.public().order_by('app_label', 'model'):
|
|
|
if model := ot.model_class():
|
|
|
objects[ot] = model.objects.count()
|
|
|
+ return objects
|
|
|
+
|
|
|
+ def get(self, request):
|
|
|
+ stats = self._get_stats()
|
|
|
+ django_apps = get_installed_apps()
|
|
|
+ config = get_config()
|
|
|
+ plugins = get_installed_plugins()
|
|
|
+ objects = self._get_object_counts()
|
|
|
|
|
|
# Raw data export
|
|
|
if 'export' in request.GET:
|
|
|
+ db_schema = get_db_schema()
|
|
|
stats['netbox_release'] = stats['netbox_release'].asdict()
|
|
|
params = [param.name for param in PARAMS]
|
|
|
data = {
|
|
|
@@ -733,6 +741,12 @@ class SystemView(UserPassesTestMixin, View):
|
|
|
'objects': {
|
|
|
f'{ot.app_label}.{ot.model}': count for ot, count in objects.items()
|
|
|
},
|
|
|
+ 'db_schema': {
|
|
|
+ table['name']: {
|
|
|
+ 'columns': table['columns'],
|
|
|
+ 'indexes': table['indexes'],
|
|
|
+ } for table in db_schema
|
|
|
+ },
|
|
|
}
|
|
|
response = HttpResponse(json.dumps(data, cls=ConfigJSONEncoder, indent=4), content_type='text/json')
|
|
|
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
|
|
|
@@ -752,6 +766,61 @@ class SystemView(UserPassesTestMixin, View):
|
|
|
})
|
|
|
|
|
|
|
|
|
+class SystemDBSchemaView(UserPassesTestMixin, View):
|
|
|
+
|
|
|
+ def test_func(self):
|
|
|
+ return self.request.user.is_superuser
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _get_db_schema_groups(db_schema):
|
|
|
+ plugin_app_labels = {
|
|
|
+ app_config.label
|
|
|
+ for app_config in django_apps_registry.get_app_configs()
|
|
|
+ if isinstance(app_config, PluginConfig)
|
|
|
+ }
|
|
|
+ # Sort longest-first so "netbox_branching" matches before "netbox"
|
|
|
+ sorted_plugin_labels = sorted(plugin_app_labels, key=len, reverse=True)
|
|
|
+ groups = {}
|
|
|
+ for table in db_schema:
|
|
|
+ matched_plugin = next(
|
|
|
+ (label for label in sorted_plugin_labels if table['name'].startswith(label + '_')),
|
|
|
+ None,
|
|
|
+ )
|
|
|
+ if matched_plugin:
|
|
|
+ prefix = matched_plugin
|
|
|
+ elif '_' in table['name']:
|
|
|
+ prefix = table['name'].split('_')[0]
|
|
|
+ else:
|
|
|
+ prefix = 'other'
|
|
|
+ groups.setdefault(prefix, []).append(table)
|
|
|
+ return sorted(
|
|
|
+ [
|
|
|
+ {
|
|
|
+ 'name': name,
|
|
|
+ 'tables': tables,
|
|
|
+ 'index_count': sum(len(t['indexes']) for t in tables),
|
|
|
+ 'is_plugin': name in plugin_app_labels,
|
|
|
+ }
|
|
|
+ for name, tables in groups.items()
|
|
|
+ ],
|
|
|
+ key=lambda g: (g['is_plugin'], g['name']),
|
|
|
+ )
|
|
|
+
|
|
|
+ def get(self, request):
|
|
|
+ db_schema = get_db_schema()
|
|
|
+ db_schema_groups = self._get_db_schema_groups(db_schema)
|
|
|
+ db_schema_stats = {
|
|
|
+ 'total_tables': len(db_schema),
|
|
|
+ 'total_columns': sum(len(t['columns']) for t in db_schema),
|
|
|
+ 'total_indexes': sum(len(t['indexes']) for t in db_schema),
|
|
|
+ }
|
|
|
+ return render(request, 'core/htmx/system_db_schema.html', {
|
|
|
+ 'db_schema': db_schema,
|
|
|
+ 'db_schema_groups': db_schema_groups,
|
|
|
+ 'db_schema_stats': db_schema_stats,
|
|
|
+ })
|
|
|
+
|
|
|
+
|
|
|
#
|
|
|
# Plugins
|
|
|
#
|