From e482aa4e92774a6d86aee11b77cb797d1a207079 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 10 Feb 2024 00:58:15 -0800 Subject: [PATCH] Status api view --- Dockerfile | 3 +- src/documents/tests/test_api_status.py | 34 ++++++++++++ src/documents/views.py | 76 ++++++++++++++++++++++++++ src/paperless/urls.py | 6 ++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/documents/tests/test_api_status.py diff --git a/Dockerfile b/Dockerfile index e113f975c..963aedbd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,7 +59,8 @@ ARG GS_VERSION=10.02.1 ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ # Ignore warning from Whitenoise - PYTHONWARNINGS="ignore:::django.http.response:517" + PYTHONWARNINGS="ignore:::django.http.response:517" \ + PNGX_CONTAINERIZED=1 # # Begin installation and configuration diff --git a/src/documents/tests/test_api_status.py b/src/documents/tests/test_api_status.py new file mode 100644 index 000000000..7f1340ed8 --- /dev/null +++ b/src/documents/tests/test_api_status.py @@ -0,0 +1,34 @@ +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.test import APITestCase + +from paperless import version + + +class TestSystemStatusView(APITestCase): + ENDPOINT = "/api/status/" + + def test_system_status_insufficient_permissions(self): + response = self.client.get(self.ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_system_status(self): + user = User.objects.create_superuser( + username="temp_admin", + ) + self.client.force_login(user) + response = self.client.get(self.ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["pngx_version"], version.__full_version_str__) + self.assertIsNotNone(response.data["server_os"]) + self.assertEqual(response.data["install_type"], "bare-metal") + self.assertIsNotNone(response.data["storage"]["total"]) + self.assertIsNotNone(response.data["storage"]["available"]) + self.assertEqual(response.data["database"]["type"], "sqlite") + self.assertIsNotNone(response.data["database"]["url"]) + self.assertEqual(response.data["database"]["status"], "OK") + self.assertIsNone(response.data["database"]["error"]) + self.assertIsNotNone(response.data["database"]["migration_status"]) + self.assertEqual(response.data["redis"]["url"], "redis://localhost:6379") + self.assertEqual(response.data["redis"]["status"], "ERROR") + self.assertIsNotNone(response.data["redis"]["error"]) diff --git a/src/documents/views.py b/src/documents/views.py index 5c84d5ea8..162a053cc 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -2,6 +2,7 @@ import itertools import json import logging import os +import platform import re import tempfile import urllib @@ -15,6 +16,9 @@ from urllib.parse import quote import pathvalidate from django.conf import settings from django.contrib.auth.models import User +from django.db import connections +from django.db.migrations.loader import MigrationLoader +from django.db.migrations.recorder import MigrationRecorder from django.db.models import Case from django.db.models import Count from django.db.models import IntegerField @@ -40,6 +44,7 @@ from django.views.generic import TemplateView from django_filters.rest_framework import DjangoFilterBackend from langdetect import detect from packaging import version as packaging_version +from redis import Redis from rest_framework import parsers from rest_framework.decorators import action from rest_framework.exceptions import NotFound @@ -1539,3 +1544,74 @@ class CustomFieldViewSet(ModelViewSet): model = CustomField queryset = CustomField.objects.all().order_by("-created") + + +class SystemStatusView(GenericAPIView, PassUserMixin): + permission_classes = (IsAuthenticated,) + + def get(self, request, format=None): + if not request.user.has_perm("admin.view_logentry"): + return HttpResponseForbidden("Insufficient permissions") + + current_version = version.__full_version_str__ + + media_stats = os.statvfs(settings.MEDIA_ROOT) + + db_conn = connections["default"] + db_url = db_conn.settings_dict["NAME"] + loader = MigrationLoader(connection=db_conn) + all_migrations = [f"{app}.{name}" for app, name in loader.graph.nodes] + applied_migrations = [ + f"{m.app}.{m.name}" + for m in MigrationRecorder.Migration.objects.all().order_by("id") + ] + db_error = None + try: + db_conn.cursor() + db_status = "OK" + except Exception as e: + db_status = "ERROR" + db_error = str(e) + + redis_url = settings._CELERY_REDIS_URL + redis_error = None + with Redis.from_url(url=redis_url) as client: + try: + client.ping() + redis_status = "OK" + except Exception as e: + redis_status = "ERROR" + redis_error = str(e) + + return Response( + { + "pngx_version": current_version, + "server_os": platform.platform(), + "install_type": ( + "containerized" + if os.environ.get("PNGX_CONTAINERIZED") == "1" + else "bare-metal" + ), + "storage": { + "total": media_stats.f_frsize * media_stats.f_blocks, + "available": media_stats.f_frsize * media_stats.f_bavail, + }, + "database": { + "type": db_conn.vendor, + "url": db_url, + "status": db_status, + "error": db_error, + "migration_status": { + "latest_migration": applied_migrations[-1], + "unapplied_migrations": [ + m for m in all_migrations if m not in applied_migrations + ], + }, + }, + "redis": { + "url": redis_url, + "status": redis_status, + "error": redis_error, + }, + }, + ) diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 142f2792d..12b049918 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -32,6 +32,7 @@ from documents.views import SharedLinkView from documents.views import ShareLinkViewSet from documents.views import StatisticsView from documents.views import StoragePathViewSet +from documents.views import SystemStatusView from documents.views import TagViewSet from documents.views import TasksViewSet from documents.views import UiSettingsView @@ -147,6 +148,11 @@ urlpatterns = [ ProfileView.as_view(), name="profile_view", ), + re_path( + "^status/", + SystemStatusView.as_view(), + name="system_status", + ), *api_router.urls, ], ),