diff --git a/api/tacticalrmm/core/decorators.py b/api/tacticalrmm/core/decorators.py index c77dde37fa..1be86c3ec9 100644 --- a/api/tacticalrmm/core/decorators.py +++ b/api/tacticalrmm/core/decorators.py @@ -1,9 +1,11 @@ import json +from functools import wraps from django.conf import settings from django.http import HttpResponse +# TODO deprecated def monitoring_view(function): def wrap(request, *args, **kwargs): if request.method != "POST": @@ -29,3 +31,25 @@ def wrap(request, *args, **kwargs): wrap.__doc__ = function.__doc__ wrap.__name__ = function.__name__ return wrap + + +def monitoring_view_v2(function): + @wraps(function) + def wrap(request, *args, **kwargs): + if request.method != "GET": + return HttpResponse("Invalid request type\n", status=400) + + http_token = request.META.get("HTTP_X_MON_TOKEN") + if not http_token: + return HttpResponse("Missing X-Mon-Token header\n", status=401) + + mon_token = getattr(settings, "MON_TOKEN", "") + if not mon_token: + return HttpResponse("Missing mon token\n", status=401) + + if http_token != mon_token: + return HttpResponse("Not authenticated\n", status=401) + + return function(request, *args, **kwargs) + + return wrap diff --git a/api/tacticalrmm/core/urls.py b/api/tacticalrmm/core/urls.py index e87c1f9dcb..dbe14a3e44 100644 --- a/api/tacticalrmm/core/urls.py +++ b/api/tacticalrmm/core/urls.py @@ -20,7 +20,8 @@ path("urlaction/run/test/", views.RunTestURLAction.as_view()), path("smstest/", views.TwilioSMSTest.as_view()), path("clearcache/", views.clear_cache), - path("status/", views.status), + path("status/", views.status), # TODO deprecated + path("v2/status/", views.status_v2), path("openai/generate/", views.OpenAICodeCompletion.as_view()), path("webtermperms/", views.webterm_perms), ] diff --git a/api/tacticalrmm/core/views.py b/api/tacticalrmm/core/views.py index eff8147a51..abd628d386 100644 --- a/api/tacticalrmm/core/views.py +++ b/api/tacticalrmm/core/views.py @@ -20,7 +20,7 @@ from rest_framework.response import Response from rest_framework.views import APIView -from core.decorators import monitoring_view +from core.decorators import monitoring_view, monitoring_view_v2 from core.tasks import sync_mesh_perms_task from core.utils import ( get_core_settings, @@ -32,6 +32,7 @@ from logs.models import AuditLog from tacticalrmm.constants import AuditActionType, PAStatus from tacticalrmm.helpers import get_certs, notify_error +from tacticalrmm.logger import logger from tacticalrmm.permissions import ( _has_perm_on_agent, _has_perm_on_client, @@ -557,6 +558,72 @@ def post(self, request): return Response(msg) +@csrf_exempt +@monitoring_view_v2 +def status_v2(request): + from agents.models import Agent + from clients.models import Client, Site + from tacticalrmm.helpers import get_nats_ports + from tacticalrmm.utils import get_celery_queue_len, localhost_port_is_open + + disk_usage: int = round(psutil.disk_usage("/").percent) + mem_usage: int = round(psutil.virtual_memory().percent) + + cert_file, _ = get_certs() + cert_bytes = Path(cert_file).read_bytes() + + cert = x509.load_pem_x509_certificate(cert_bytes) + delta = cert.not_valid_after_utc - djangotime.now() + + redis_url = f"redis://{settings.REDIS_HOST}" + redis_ping = False + with suppress(Exception): + with from_url(redis_url) as conn: + conn.ping() + redis_ping = True + + celery_queue_health = "healthy" + try: + queue_len = get_celery_queue_len() + except RuntimeError as e: + queue_len = -1 + celery_queue_health = "unhealthy" + logger.error(f"Error getting celery queue length: {e}") + + nats_std_port, nats_ws_port = get_nats_ports() + mesh_port = getattr(settings, "MESH_PORT", 4430) + + ret = { + "version": settings.TRMM_VERSION, + "latest_agent_version": settings.LATEST_AGENT_VER, + "agent_count": Agent.objects.count(), + "client_count": Client.objects.count(), + "site_count": Site.objects.count(), + "disk_usage_percent": disk_usage, + "mem_usage_percent": mem_usage, + "days_until_cert_expires": delta.days, + "cert_expired": delta.days < 0, + "redis_ping": redis_ping, + "celery_queue_len": queue_len, + "celery_queue_health": celery_queue_health, + "nats_std_ping": localhost_port_is_open(nats_std_port), + "nats_ws_ping": localhost_port_is_open(nats_ws_port), + "mesh_ping": localhost_port_is_open(mesh_port), + "services_running": { + "mesh": sysd_svc_is_running("meshcentral.service"), + "daphne": sysd_svc_is_running("daphne.service"), + "celery": sysd_svc_is_running("celery.service"), + "celerybeat": sysd_svc_is_running("celerybeat.service"), + "redis": sysd_svc_is_running("redis-server.service"), + "nats": sysd_svc_is_running("nats.service"), + "nats-api": sysd_svc_is_running("nats-api.service"), + } + } + + return JsonResponse(ret, json_dumps_params={"indent": 2}) + + +## TODO deprecated @csrf_exempt @monitoring_view def status(request): diff --git a/api/tacticalrmm/tacticalrmm/utils.py b/api/tacticalrmm/tacticalrmm/utils.py index 6c7be4152a..4897f51adb 100644 --- a/api/tacticalrmm/tacticalrmm/utils.py +++ b/api/tacticalrmm/tacticalrmm/utils.py @@ -1,9 +1,10 @@ import json import os +import re +import socket import subprocess import tempfile import time -import re from contextlib import contextmanager from typing import TYPE_CHECKING, List, Literal, Optional, Union from zoneinfo import ZoneInfo @@ -21,6 +22,7 @@ from agents.models import Agent from core.utils import get_core_settings, token_is_valid from logs.models import DebugLog +from tacticalrmm.celery import app as celery_app from tacticalrmm.constants import ( MONTH_DAYS, MONTHS, @@ -41,8 +43,8 @@ ) if TYPE_CHECKING: - from clients.models import Client, Site from alerts.models import Alert + from clients.models import Client, Site def generate_winagent_exe( @@ -468,3 +470,21 @@ def runcmd_placeholder_text() -> dict[str, str]: ), } return ret + + +def get_celery_queue_len(): + try: + with celery_app.pool.acquire(block=True) as conn: + return conn.default_channel.client.llen("celery") + except Exception as e: + raise RuntimeError(f"Error getting celery queue length: {e}") + + +def localhost_port_is_open(port): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(1) + s.connect(("127.0.0.1", port)) + return True + except (socket.timeout, ConnectionRefusedError): + return False