Просмотр исходного кода

Closes #4918: Add a REST API endpoint which returns NetBox's current operational status

Jeremy Stretch 5 лет назад
Родитель
Сommit
80c142ab7c

+ 3 - 1
docs/release-notes/version-2.10.md

@@ -46,6 +46,7 @@ All end-to-end cable paths are now cached using the new CablePath model. This al
 * [#1692](https://github.com/netbox-community/netbox/issues/1692) - Allow assigment of inventory items to parent items in web UI
 * [#2179](https://github.com/netbox-community/netbox/issues/2179) - Support the assignment of multiple port numbers for services
 * [#4897](https://github.com/netbox-community/netbox/issues/4897) - Allow filtering by content type identified as `<app>.<model>` string
+* [#4918](https://github.com/netbox-community/netbox/issues/4918) - Add a REST API endpoint (`/api/status/`) which returns NetBox's current operational status
 * [#4956](https://github.com/netbox-community/netbox/issues/4956) - Include inventory items on primary device view
 * [#5003](https://github.com/netbox-community/netbox/issues/5003) - CSV import now accepts slug values for choice fields
 * [#5146](https://github.com/netbox-community/netbox/issues/5146) - Add custom fields support for cables, power panels, rack reservations, and virtual chassis
@@ -63,7 +64,8 @@ All end-to-end cable paths are now cached using the new CablePath model. This al
 ### REST API Changes
 
 * Added support for `PUT`, `PATCH`, and `DELETE` operations on list endpoints (bulk update and delete)
-* Added `/extras/content-types/` endpoint for Django ContentTypes
+* Added the `/extras/content-types/` endpoint for Django ContentTypes
+* Added the `/status/` endpoint to convey NetBox's current status
 * circuits.CircuitTermination:
   * Added the `/trace/` endpoint
   * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)

+ 46 - 0
netbox/netbox/api/views.py

@@ -1,11 +1,18 @@
 import logging
+import platform
 
+from django import __version__ as DJANGO_VERSION
+from django.apps import apps
+from django.conf import settings
 from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
 from django.db import transaction
 from django.db.models import ProtectedError
+from django_rq.queues import get_connection
 from rest_framework import mixins, status
 from rest_framework.response import Response
+from rest_framework.views import APIView
 from rest_framework.viewsets import GenericViewSet
+from rq.worker import Worker
 
 from netbox.api import BulkOperationSerializer
 from netbox.api.exceptions import SerializerNotFound
@@ -218,3 +225,42 @@ class ModelViewSet(mixins.CreateModelMixin,
         logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")
 
         return super().perform_destroy(instance)
+
+
+#
+# Views
+#
+
+class StatusView(APIView):
+    """
+    Provide a lightweight read-only endpoint for conveying NetBox's current operational status.
+    """
+    permission_classes = []
+
+    def get(self, request):
+        # Gather the version number from all installed Django apps
+        installed_apps = {}
+        for app_config in apps.get_app_configs():
+            app = app_config.module
+            version = getattr(app, 'VERSION', getattr(app, '__version__', None))
+            if version:
+                if type(version) is tuple:
+                    version = '.'.join(str(n) for n in version)
+                installed_apps[app_config.name] = version
+        installed_apps = {k: v for k, v in sorted(installed_apps.items())}
+
+        # Gather installed plugins
+        plugins = {}
+        for plugin_name in settings.PLUGINS:
+            plugin_config = apps.get_app_config(plugin_name)
+            plugins[plugin_name] = getattr(plugin_config, 'version', None)
+        plugins = {k: v for k, v in sorted(plugins.items())}
+
+        return Response({
+            'django-version': DJANGO_VERSION,
+            'installed-apps': installed_apps,
+            'netbox-version': settings.VERSION,
+            'plugins': plugins,
+            'python-version': platform.python_version(),
+            'rq-workers-running': Worker.count(get_connection('default')),
+        })

+ 6 - 1
netbox/netbox/tests/test_api.py

@@ -6,8 +6,13 @@ from utilities.testing import APITestCase
 class AppTest(APITestCase):
 
     def test_root(self):
-
         url = reverse('api-root')
         response = self.client.get('{}?format=api'.format(url), **self.header)
 
         self.assertEqual(response.status_code, 200)
+
+    def test_status(self):
+        url = reverse('api-status')
+        response = self.client.get('{}?format=api'.format(url), **self.header)
+
+        self.assertEqual(response.status_code, 200)

+ 2 - 0
netbox/netbox/urls.py

@@ -6,6 +6,7 @@ from drf_yasg import openapi
 from drf_yasg.views import get_schema_view
 
 from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
+from netbox.api.views import StatusView
 from netbox.views import APIRootView, HomeView, StaticMediaFailureView, SearchView
 from users.views import LoginView, LogoutView
 from .admin import admin_site
@@ -55,6 +56,7 @@ _patterns = [
     path('api/tenancy/', include('tenancy.api.urls')),
     path('api/users/', include('users.api.urls')),
     path('api/virtualization/', include('virtualization.api.urls')),
+    path('api/status/', StatusView.as_view(), name='api-status'),
     path('api/docs/', schema_view.with_ui('swagger'), name='api_docs'),
     path('api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'),
     re_path(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'),

+ 1 - 0
netbox/netbox/views.py

@@ -342,6 +342,7 @@ class APIRootView(APIView):
             ('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)),
+            ('status', reverse('api-status', request=request, format=format)),
             ('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
             ('users', reverse('users-api:api-root', request=request, format=format)),
             ('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),