Closes #19773: Extend system view (#20078)

This commit is contained in:
Jeremy Stretch 2025-08-12 13:59:15 -04:00 committed by GitHub
parent bb57021197
commit 8238fda8ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 146 additions and 67 deletions

View File

@ -1,3 +1,4 @@
import json
import urllib.parse import urllib.parse
import uuid import uuid
from datetime import datetime from datetime import datetime
@ -366,6 +367,11 @@ class SystemTestCase(TestCase):
# Test export # Test export
response = self.client.get(f"{reverse('core:system')}?export=true") response = self.client.get(f"{reverse('core:system')}?export=true")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertIn('netbox_release', data)
self.assertIn('plugins', data)
self.assertIn('config', data)
self.assertIn('objects', data)
def test_system_view_with_config_revision(self): def test_system_view_with_config_revision(self):
ConfigRevision.objects.create() ConfigRevision.objects.create()

View File

@ -1,7 +1,7 @@
import json import json
import platform import platform
from django import __version__ as DJANGO_VERSION from django import __version__ as django_version
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
@ -23,7 +23,7 @@ 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_rq_jobs_from_status, requeue_rq_job, stop_rq_job
from netbox.config import get_config, PARAMS from netbox.config import get_config, PARAMS
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
from netbox.registry import registry from netbox.plugins.utils import get_installed_plugins
from netbox.views import generic from netbox.views import generic
from netbox.views.generic.base import BaseObjectView from netbox.views.generic.base import BaseObjectView
from netbox.views.generic.mixins import TableMixin from netbox.views.generic.mixins import TableMixin
@ -546,7 +546,7 @@ class SystemView(UserPassesTestMixin, View):
def get(self, request): def get(self, request):
# System stats # System status
psql_version = db_name = db_size = None psql_version = db_name = db_size = None
try: try:
with connection.cursor() as cursor: with connection.cursor() as cursor:
@ -561,7 +561,7 @@ class SystemView(UserPassesTestMixin, View):
pass pass
stats = { stats = {
'netbox_release': settings.RELEASE, 'netbox_release': settings.RELEASE,
'django_version': DJANGO_VERSION, 'django_version': django_version,
'python_version': platform.python_version(), 'python_version': platform.python_version(),
'postgresql_version': psql_version, 'postgresql_version': psql_version,
'database_name': db_name, 'database_name': db_name,
@ -572,16 +572,28 @@ class SystemView(UserPassesTestMixin, View):
# Configuration # Configuration
config = get_config() config = get_config()
# Plugins
plugins = get_installed_plugins()
# Object counts
objects = {}
for ot in ObjectType.objects.public().order_by('app_label', 'model'):
if model := ot.model_class():
objects[ot] = model.objects.count()
# Raw data export # Raw data export
if 'export' in request.GET: if 'export' in request.GET:
stats['netbox_release'] = stats['netbox_release'].asdict() stats['netbox_release'] = stats['netbox_release'].asdict()
params = [param.name for param in PARAMS] params = [param.name for param in PARAMS]
data = { data = {
**stats, **stats,
'plugins': registry['plugins']['installed'], 'plugins': plugins,
'config': { 'config': {
k: getattr(config, k) for k in sorted(params) k: getattr(config, k) for k in sorted(params)
}, },
'objects': {
f'{ot.app_label}.{ot.model}': count for ot, count in objects.items()
},
} }
response = HttpResponse(json.dumps(data, cls=ConfigJSONEncoder, indent=4), content_type='text/json') response = HttpResponse(json.dumps(data, cls=ConfigJSONEncoder, indent=4), content_type='text/json')
response['Content-Disposition'] = 'attachment; filename="netbox.json"' response['Content-Disposition'] = 'attachment; filename="netbox.json"'
@ -595,6 +607,8 @@ class SystemView(UserPassesTestMixin, View):
return render(request, 'core/system.html', { return render(request, 'core/system.html', {
'stats': stats, 'stats': stats,
'config': config, 'config': config,
'plugins': plugins,
'objects': objects,
}) })

View File

@ -8,82 +8,141 @@
{% block controls %} {% block controls %}
<a href="?export=true" class="btn btn-purple"> <a href="?export=true" class="btn btn-purple">
<i class="mdi mdi-download"></i> {% trans "Export" %} <i class="mdi mdi-download"></i> {% trans "Export All" %}
</a> </a>
{% endblock controls %} {% endblock controls %}
{% block tabs %} {% block tabs %}
<ul class="nav nav-tabs px-3"> <ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link active" role="tab">{% trans "Status" %}</a> <a class="nav-link active" id="status-tab" data-bs-toggle="tab" data-bs-target="#status-panel" type="button" role="tab" aria-selected="true">
{% trans "Status" %}
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="config-tab" data-bs-toggle="tab" data-bs-target="#config-panel" type="button" role="tab">
{% trans "Config" %}
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="plugins-tab" data-bs-toggle="tab" data-bs-target="#plugins-panel" type="button" role="tab">
{% trans "Plugins" %}
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="objects-tab" data-bs-toggle="tab" data-bs-target="#objects-panel" type="button" role="tab">
{% trans "Object Counts" %}
</a>
</li> </li>
</ul> </ul>
{% endblock tabs %} {% endblock tabs %}
{% block content %} {% block content %}
{# System status #} {# Status panel #}
<div class="row mb-3"> <div class="tab-pane show active" id="status-panel" role="tabpanel" aria-labelledby="status-tab">
<div class="col"> <div class="row mb-3">
<div class="card"> <div class="col">
<h2 class="card-header">{% trans "System Status" %}</h2> <div class="card">
<table class="table table-hover attr-table"> <h2 class="card-header">{% trans "System Status" %}</h2>
<tr> <table class="table table-hover attr-table">
<th scope="row">{% trans "NetBox release" %}</th> <tr>
<td> <th scope="row">{% trans "NetBox release" %}</th>
{{ stats.netbox_release.name }} <td>
{% if stats.netbox_release.published %} {{ stats.netbox_release.name }}
({{ stats.netbox_release.published|isodate }}) {% if stats.netbox_release.published %}
{% endif %} ({{ stats.netbox_release.published|isodate }})
</td> {% endif %}
</tr> </td>
<tr> </tr>
<th scope="row">{% trans "Python version" %}</th> <tr>
<td>{{ stats.python_version }}</td> <th scope="row">{% trans "Python version" %}</th>
</tr> <td>{{ stats.python_version }}</td>
<tr> </tr>
<th scope="row">{% trans "Django version" %}</th> <tr>
<td>{{ stats.django_version }}</td> <th scope="row">{% trans "Django version" %}</th>
</tr> <td>{{ stats.django_version }}</td>
<tr> </tr>
<th scope="row">{% trans "PostgreSQL version" %}</th> <tr>
<td>{{ stats.postgresql_version }}</td> <th scope="row">{% trans "PostgreSQL version" %}</th>
</tr> <td>{{ stats.postgresql_version }}</td>
<tr> </tr>
<th scope="row">{% trans "Database name" %}</th> <tr>
<td>{{ stats.database_name }}</td> <th scope="row">{% trans "Database name" %}</th>
</tr> <td>{{ stats.database_name }}</td>
<tr> </tr>
<th scope="row">{% trans "Database size" %}</th> <tr>
<td> <th scope="row">{% trans "Database size" %}</th>
{% if stats.database_size %} <td>
{{ stats.database_size }} {% if stats.database_size %}
{% else %} {{ stats.database_size }}
<span class="text-muted">{% trans "Unavailable" %}</span> {% else %}
{% endif %} <span class="text-muted">{% trans "Unavailable" %}</span>
</td> {% endif %}
</tr> </td>
<tr> </tr>
<th scope="row">{% trans "RQ workers" %}</th> <tr>
<td> <th scope="row">{% trans "RQ workers" %}</th>
<a href="{% url 'core:background_queue_list' %}">{{ stats.rq_worker_count }}</a> <td>
({% trans "default queue" %}) <a href="{% url 'core:background_queue_list' %}">{{ stats.rq_worker_count }}</a>
</td> ({% trans "default queue" %})
</tr> </td>
<tr> </tr>
<th scope="row">{% trans "System time" %}</th> <tr>
<td>{% now 'Y-m-d H:i:s T' %}</td> <th scope="row">{% trans "System time" %}</th>
</tr> <td>{% now 'Y-m-d H:i:s T' %}</td>
</table> </tr>
</table>
</div>
</div> </div>
</div> </div>
</div> </div>
{# Configuration #} {# Config panel #}
<div class="row mb-3"> <div class="tab-pane" id="config-panel" role="tabpanel" aria-labelledby="config-tab">
<div class="col col-md-12"> <div class="row mb-3">
<div class="card"> <div class="col">
<h2 class="card-header">{% trans "Current Configuration" %}</h2> <div class="card">
{% include 'core/inc/config_data.html' %} <h2 class="card-header">{% trans "Current Configuration" %}</h2>
{% include 'core/inc/config_data.html' %}
</div>
</div>
</div>
</div>
{# Plugins panel #}
<div class="tab-pane" id="plugins-panel" role="tabpanel" aria-labelledby="plugins-tab">
<div class="row mb-3">
<div class="col">
<div class="card">
<h2 class="card-header">{% trans "Installed Plugins" %}</h2>
<table class="table table-hover attr-table">
{% for plugin, version in plugins.items %}
<tr>
<td>{{ plugin }}</td>
<td>{{ version }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
{# Objects panel #}
<div class="tab-pane" id="objects-panel" role="tabpanel" aria-labelledby="objects-tab">
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "Object Counts" %}</h2>
<table class="table table-hover attr-table">
{% for object_type, count in objects.items %}
<tr{% if not count %} class="text-muted"{% endif %}>
<td>{{ object_type }}</td>
<td>{{ count }}</td>
</tr>
{% endfor %}
</table>
</div>
</div> </div>
</div> </div>
</div> </div>