From 5dac1efc304bb040a88f33f94771444adf901385 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Fri, 23 Feb 2024 20:46:28 +0000 Subject: [PATCH] sync mesh users/perms with trmm #182 --- api/tacticalrmm/accounts/models.py | 8 ++ api/tacticalrmm/accounts/utils.py | 6 + api/tacticalrmm/accounts/views.py | 7 +- api/tacticalrmm/agents/models.py | 6 +- api/tacticalrmm/agents/views.py | 10 +- api/tacticalrmm/apiv3/views.py | 2 + api/tacticalrmm/automation/tests.py | 2 +- .../commands/clear_redis_celery_locks.py | 2 + api/tacticalrmm/core/mesh_utils.py | 105 ++++++++++++++ .../0042_coresettings_mesh_company_name.py | 18 +++ .../0043_coresettings_sync_mesh_with_trmm.py | 18 +++ api/tacticalrmm/core/models.py | 4 +- api/tacticalrmm/core/tasks.py | 135 +++++++++++++++++- api/tacticalrmm/core/views.py | 2 + api/tacticalrmm/tacticalrmm/celery.py | 4 + api/tacticalrmm/tacticalrmm/constants.py | 1 + api/tacticalrmm/tacticalrmm/logger.py | 3 + api/tacticalrmm/tacticalrmm/permissions.py | 6 +- api/tacticalrmm/tacticalrmm/settings.py | 27 +++- 19 files changed, 352 insertions(+), 14 deletions(-) create mode 100644 api/tacticalrmm/core/mesh_utils.py create mode 100644 api/tacticalrmm/core/migrations/0042_coresettings_mesh_company_name.py create mode 100644 api/tacticalrmm/core/migrations/0043_coresettings_sync_mesh_with_trmm.py create mode 100644 api/tacticalrmm/tacticalrmm/logger.py diff --git a/api/tacticalrmm/accounts/models.py b/api/tacticalrmm/accounts/models.py index b561ca51cd..e37bf918be 100644 --- a/api/tacticalrmm/accounts/models.py +++ b/api/tacticalrmm/accounts/models.py @@ -64,6 +64,14 @@ class User(AbstractUser, BaseAuditModel): on_delete=models.SET_NULL, ) + @property + def mesh_user_id(self): + return f"user//{self.mesh_username}" + + @property + def mesh_username(self): + return f"{self.username}___{self.pk}" + @staticmethod def serialize(user): # serializes the task and returns json diff --git a/api/tacticalrmm/accounts/utils.py b/api/tacticalrmm/accounts/utils.py index e87b786fcd..37e552b7c6 100644 --- a/api/tacticalrmm/accounts/utils.py +++ b/api/tacticalrmm/accounts/utils.py @@ -1,8 +1,10 @@ from typing import TYPE_CHECKING + from django.conf import settings if TYPE_CHECKING: from django.http import HttpRequest + from accounts.models import User @@ -16,3 +18,7 @@ def is_root_user(*, request: "HttpRequest", user: "User") -> bool: getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER ) return root or demo + + +def is_superuser(user: "User") -> bool: + return user.role and getattr(user.role, "is_superuser") diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index f16fc30e83..9fe0f45ae6 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -11,6 +11,7 @@ from rest_framework.views import APIView from accounts.utils import is_root_user +from core.tasks import sync_mesh_perms_task from logs.models import AuditLog from tacticalrmm.helpers import notify_error @@ -133,6 +134,7 @@ def post(self, request): user.role = role user.save() + sync_mesh_perms_task.delay() return Response(user.username) @@ -153,6 +155,7 @@ def put(self, request, pk): serializer = UserSerializer(instance=user, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() + sync_mesh_perms_task.delay() return Response("ok") @@ -162,7 +165,7 @@ def delete(self, request, pk): return notify_error("The root user cannot be deleted from the UI") user.delete() - + sync_mesh_perms_task.delay() return Response("ok") @@ -243,11 +246,13 @@ def put(self, request, pk): serializer = RoleSerializer(instance=role, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() + sync_mesh_perms_task.delay() return Response("Role was edited") def delete(self, request, pk): role = get_object_or_404(Role, pk=pk) role.delete() + sync_mesh_perms_task.delay() return Response("Role was removed") diff --git a/api/tacticalrmm/agents/models.py b/api/tacticalrmm/agents/models.py index f17bddef63..5fdbc3c331 100644 --- a/api/tacticalrmm/agents/models.py +++ b/api/tacticalrmm/agents/models.py @@ -20,7 +20,7 @@ from agents.utils import get_agent_url from checks.models import CheckResult from core.models import TZ_CHOICES -from core.utils import get_core_settings, send_command_with_mesh +from core.utils import _b64_to_hex, get_core_settings, send_command_with_mesh from logs.models import BaseAuditModel, DebugLog, PendingAction from tacticalrmm.constants import ( AGENT_STATUS_OFFLINE, @@ -452,6 +452,10 @@ def serial_number(self) -> str: except: return "" + @property + def hex_mesh_node_id(self) -> str: + return _b64_to_hex(self.mesh_node_id) + @classmethod def online_agents(cls, min_version: str = "") -> "List[Agent]": if min_version: diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index e4046f8ee5..a130269509 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -21,6 +21,7 @@ from rest_framework.response import Response from rest_framework.views import APIView +from core.tasks import sync_mesh_perms_task from core.utils import ( get_core_settings, get_mesh_ws_url, @@ -258,6 +259,7 @@ def put(self, request, agent_id): serializer.is_valid(raise_exception=True) serializer.save() + sync_mesh_perms_task.delay() return Response("The agent was updated successfully") # uninstall agent @@ -283,6 +285,7 @@ def delete(self, request, agent_id): message=f"Unable to remove agent {name} from meshcentral database: {e}", log_type=DebugLogType.AGENT_ISSUES, ) + sync_mesh_perms_task.delay() return Response(f"{name} will now be uninstalled.") @@ -326,9 +329,12 @@ def get(self, request, agent_id): core = get_core_settings() if not core.mesh_disable_auto_login: - token = get_login_token( - key=core.mesh_token, user=f"user//{core.mesh_username}" + user = ( + request.user.mesh_user_id + if core.sync_mesh_with_trmm + else f"user//{core.mesh_username}" ) + token = get_login_token(key=core.mesh_token, user=user) token_param = f"login={token}&" else: token_param = "" diff --git a/api/tacticalrmm/apiv3/views.py b/api/tacticalrmm/apiv3/views.py index 42f95deec5..8d3b76b58d 100644 --- a/api/tacticalrmm/apiv3/views.py +++ b/api/tacticalrmm/apiv3/views.py @@ -20,6 +20,7 @@ from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER from checks.models import Check, CheckResult from checks.serializers import CheckRunnerGetSerializer +from core.tasks import sync_mesh_perms_task from core.utils import ( download_mesh_agent, get_core_settings, @@ -481,6 +482,7 @@ def post(self, request): ) ret = {"pk": agent.pk, "token": token.key} + sync_mesh_perms_task.delay() return Response(ret) diff --git a/api/tacticalrmm/automation/tests.py b/api/tacticalrmm/automation/tests.py index 139d954c0d..b530a7d4bb 100644 --- a/api/tacticalrmm/automation/tests.py +++ b/api/tacticalrmm/automation/tests.py @@ -126,7 +126,7 @@ def test_update_policy(self, cache_alert_template): resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - cache_alert_template.called_once() + cache_alert_template.assert_called_once() self.check_not_authenticated("put", url) diff --git a/api/tacticalrmm/core/management/commands/clear_redis_celery_locks.py b/api/tacticalrmm/core/management/commands/clear_redis_celery_locks.py index ece7fc9137..40effb3508 100644 --- a/api/tacticalrmm/core/management/commands/clear_redis_celery_locks.py +++ b/api/tacticalrmm/core/management/commands/clear_redis_celery_locks.py @@ -5,6 +5,7 @@ AGENT_OUTAGES_LOCK, ORPHANED_WIN_TASK_LOCK, RESOLVE_ALERTS_LOCK, + SYNC_MESH_PERMS_TASK_LOCK, SYNC_SCHED_TASK_LOCK, ) @@ -18,5 +19,6 @@ def handle(self, *args, **kwargs): ORPHANED_WIN_TASK_LOCK, RESOLVE_ALERTS_LOCK, SYNC_SCHED_TASK_LOCK, + SYNC_MESH_PERMS_TASK_LOCK, ): cache.delete(key) diff --git a/api/tacticalrmm/core/mesh_utils.py b/api/tacticalrmm/core/mesh_utils.py new file mode 100644 index 0000000000..de5fe4b596 --- /dev/null +++ b/api/tacticalrmm/core/mesh_utils.py @@ -0,0 +1,105 @@ +import asyncio +import json +from typing import TYPE_CHECKING, Any + +import websockets + +from accounts.utils import is_superuser +from tacticalrmm.helpers import make_random_password +from tacticalrmm.logger import logger + +if TYPE_CHECKING: + from accounts.models import User + + +def mesh_action( + *, payload: dict[str, Any], uri: str, wait=True +) -> dict[str, Any] | None: + async def _do(payload, uri: str): + async with websockets.connect(uri) as ws: + await ws.send(json.dumps(payload)) + if wait: + async for message in ws: + r = json.loads(message) + if r["action"] == payload["action"]: + return r + else: + return None + + payload["responseid"] = "meshctrl" + logger.debug(payload) + + return asyncio.run(_do(payload, uri)) + + +def update_mesh_displayname(*, user_info: dict[str, Any], uri: str) -> None: + payload = { + "action": "edituser", + "id": user_info["_id"], + "realname": user_info["full_name"], + } + mesh_action(payload=payload, uri=uri, wait=False) + logger.debug( + f"Updating user {user_info['username']} display name to: {user_info['full_name']}" + ) + + +def add_user_to_mesh(*, user_info: dict[str, Any], uri: str) -> None: + payload = { + "action": "adduser", + "username": user_info["username"], + "email": user_info["email"], + "pass": make_random_password(len=30), + "resetNextLogin": False, + "randomPassword": False, + "removeEvents": False, + } + mesh_action(payload=payload, uri=uri, wait=False) + logger.debug(f"Adding user {user_info['username']} to mesh") + if user_info["full_name"]: + update_mesh_displayname(user_info=user_info, uri=uri) + + +def delete_user_from_mesh(*, mesh_user_id: str, uri: str) -> None: + logger.debug(f"Deleting {mesh_user_id} from mesh") + payload = { + "action": "deleteuser", + "userid": mesh_user_id, + } + mesh_action(payload=payload, uri=uri, wait=False) + + +def add_agent_to_user(*, user_id: str, node_id: str, hostname: str, uri: str) -> None: + logger.debug(f"Adding agent {hostname} to {user_id}") + payload = { + "action": "adddeviceuser", + "nodeid": node_id, + "userids": [user_id], + "rights": 72, + "remove": False, + } + mesh_action(payload=payload, uri=uri, wait=False) + + +def remove_agent_from_user(*, user_id: str, node_id: str, uri: str) -> None: + logger.debug(f"Removing agent {node_id} from {user_id}") + payload = { + "action": "adddeviceuser", + "nodeid": node_id, + "userids": [user_id], + "rights": 0, + "remove": True, + } + mesh_action(payload=payload, uri=uri, wait=False) + + +def has_mesh_perms(*, user: "User") -> bool: + if user.is_superuser or is_superuser(user): + return True + + return user.role and getattr(user.role, "can_use_mesh") + + +def get_mesh_users(*, uri: str) -> dict[str, Any] | None: + payload = {"action": "users"} + return mesh_action(payload=payload, uri=uri, wait=True) diff --git a/api/tacticalrmm/core/migrations/0042_coresettings_mesh_company_name.py b/api/tacticalrmm/core/migrations/0042_coresettings_mesh_company_name.py new file mode 100644 index 0000000000..d70b26acfe --- /dev/null +++ b/api/tacticalrmm/core/migrations/0042_coresettings_mesh_company_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.10 on 2024-02-20 02:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0041_auto_20240128_0301"), + ] + + operations = [ + migrations.AddField( + model_name="coresettings", + name="mesh_company_name", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/core/migrations/0043_coresettings_sync_mesh_with_trmm.py b/api/tacticalrmm/core/migrations/0043_coresettings_sync_mesh_with_trmm.py new file mode 100644 index 0000000000..d0847b5dff --- /dev/null +++ b/api/tacticalrmm/core/migrations/0043_coresettings_sync_mesh_with_trmm.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.10 on 2024-02-23 19:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0042_coresettings_mesh_company_name"), + ] + + operations = [ + migrations.AddField( + model_name="coresettings", + name="sync_mesh_with_trmm", + field=models.BooleanField(default=True), + ), + ] diff --git a/api/tacticalrmm/core/models.py b/api/tacticalrmm/core/models.py index fac099af47..dab9b293b6 100644 --- a/api/tacticalrmm/core/models.py +++ b/api/tacticalrmm/core/models.py @@ -1,7 +1,7 @@ import smtplib from contextlib import suppress -from email.message import EmailMessage from email.headerregistry import Address +from email.message import EmailMessage from typing import TYPE_CHECKING, List, Optional, cast import requests @@ -75,6 +75,8 @@ class CoreSettings(BaseAuditModel): max_length=255, null=True, blank=True, default="TacticalRMM" ) mesh_disable_auto_login = models.BooleanField(default=False) + mesh_company_name = models.CharField(max_length=255, null=True, blank=True) + sync_mesh_with_trmm = models.BooleanField(default=True) agent_auto_update = models.BooleanField(default=True) workstation_policy = models.ForeignKey( "automation.Policy", diff --git a/api/tacticalrmm/core/tasks.py b/api/tacticalrmm/core/tasks.py index aaba88e01f..1d3aba8c7f 100644 --- a/api/tacticalrmm/core/tasks.py +++ b/api/tacticalrmm/core/tasks.py @@ -1,5 +1,6 @@ import asyncio import logging +import re from contextlib import suppress from typing import TYPE_CHECKING, Any @@ -10,6 +11,8 @@ from django.utils import timezone as djangotime from packaging import version as pyver +from accounts.models import User +from accounts.utils import is_superuser from agents.models import Agent from agents.tasks import clear_faults_task, prune_agent_history from alerts.models import Alert @@ -18,7 +21,17 @@ from checks.models import Check, CheckResult from checks.tasks import prune_check_history from clients.models import Client, Site -from core.utils import get_core_settings +from core.mesh_utils import ( + add_agent_to_user, + add_user_to_mesh, + delete_user_from_mesh, + get_mesh_users, + has_mesh_perms, + remove_agent_from_user, + update_mesh_displayname, +) +from core.models import CoreSettings +from core.utils import get_core_settings, get_mesh_ws_url from logs.models import PendingAction from logs.tasks import prune_audit_log, prune_debug_log from tacticalrmm.celery import app @@ -27,6 +40,7 @@ AGENT_STATUS_ONLINE, AGENT_STATUS_OVERDUE, RESOLVE_ALERTS_LOCK, + SYNC_MESH_PERMS_TASK_LOCK, SYNC_SCHED_TASK_LOCK, AlertSeverity, AlertType, @@ -38,6 +52,7 @@ ) from tacticalrmm.helpers import setup_nats_options from tacticalrmm.nats_utils import a_nats_cmd +from tacticalrmm.permissions import _has_perm_on_agent from tacticalrmm.utils import redis_lock if TYPE_CHECKING: @@ -361,3 +376,121 @@ def cache_db_fields_task() -> None: agents = qs.filter(site__client=client) client.failing_checks = _get_failing_data(agents) client.save(update_fields=["failing_checks"]) + + +@app.task(bind=True) +def sync_mesh_perms_task(self): + with redis_lock(SYNC_MESH_PERMS_TASK_LOCK, self.app.oid) as acquired: + if not acquired: + return f"{self.app.oid} still running" + + core = CoreSettings.objects.first() + if not core.sync_mesh_with_trmm: + return + + try: + uri = get_mesh_ws_url() + company_name = core.mesh_company_name + mesh_users_raw = get_mesh_users(uri=uri)["users"] + mesh_users_dict = { + i["_id"]: i for i in mesh_users_raw if re.search(r".*___\d+", i["_id"]) + } + + users = User.objects.select_related("role").filter( + agent=None, + is_installer_user=False, + ) + + trmm_user_ids = set() + + for user in users: + if not has_mesh_perms(user=user): + logger.debug(f"No mesh perms for {user}") + continue + + if user.is_superuser or is_superuser(user): + # superusers get access to all agents no matter perms + trmm_agents = [ + { + "node_id": f"node//{agent.hex_mesh_node_id}", + "hostname": agent.hostname, + } + for agent in Agent.objects.only("mesh_node_id", "hostname") + ] + else: + trmm_agents = [ + { + "node_id": f"node//{agent.hex_mesh_node_id}", + "hostname": agent.hostname, + } + for agent in Agent.objects.defer(*AGENT_DEFER) + if _has_perm_on_agent(user, agent.agent_id) + ] + + user_info = { + "_id": user.mesh_user_id, + "username": user.mesh_username, + "email": user.email or f"{user.username}@example.com", + "full_name": " ".join( + filter( + None, + [user.first_name, user.last_name] + + [f"- {company_name}" if company_name else None], + ) + ), + "links": trmm_agents, + } + + trmm_user_ids.add(user.mesh_user_id) + + # Handle new users and assign agents to them + if user.mesh_user_id not in mesh_users_dict: + add_user_to_mesh(user_info=user_info, uri=uri) + for agent in trmm_agents: + add_agent_to_user( + user_id=user.mesh_user_id, + node_id=agent["node_id"], + hostname=agent["hostname"], + uri=uri, + ) + else: + # For existing users, check and update agent perms + existing_mesh_user = mesh_users_dict[user.mesh_user_id] + mesh_agents_dict = existing_mesh_user.get("links", {}) + trmm_agent_ids = {agent["node_id"] for agent in trmm_agents} + + for agent in trmm_agents: + if agent["node_id"] not in mesh_agents_dict: + add_agent_to_user( + user_id=user.mesh_user_id, + node_id=agent["node_id"], + hostname=agent["hostname"], + uri=uri, + ) + + for mesh_agent_id in mesh_agents_dict: + if mesh_agent_id not in trmm_agent_ids: + remove_agent_from_user( + user_id=user.mesh_user_id, + node_id=mesh_agent_id, + uri=uri, + ) + + # handle diplay name + try: + mesh_displayname = existing_mesh_user["realname"] + except KeyError: + logger.debug("Adding Display Name to mesh.") + update_mesh_displayname(user_info=user_info, uri=uri) + else: + if mesh_displayname != user_info["full_name"]: + logger.debug("Display names don't match. Syncing.") + update_mesh_displayname(user_info=user_info, uri=uri) + + # Remove users from mesh not present in trmm + for mesh_user_id in mesh_users_dict: + if mesh_user_id not in trmm_user_ids: + delete_user_from_mesh(mesh_user_id=mesh_user_id, uri=uri) + + except Exception as e: + logger.error(str(e)) diff --git a/api/tacticalrmm/core/views.py b/api/tacticalrmm/core/views.py index dccddb498e..ac963b7900 100644 --- a/api/tacticalrmm/core/views.py +++ b/api/tacticalrmm/core/views.py @@ -21,6 +21,7 @@ from rest_framework.views import APIView from core.decorators import monitoring_view +from core.tasks import sync_mesh_perms_task from core.utils import get_core_settings, sysd_svc_is_running, token_is_valid from logs.models import AuditLog from tacticalrmm.constants import AuditActionType, PAStatus @@ -60,6 +61,7 @@ def put(self, request): serializer = CoreSettingsSerializer(instance=coresettings, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() + sync_mesh_perms_task.delay() return Response("ok") diff --git a/api/tacticalrmm/tacticalrmm/celery.py b/api/tacticalrmm/tacticalrmm/celery.py index 30b85f2ba0..100d7ba581 100644 --- a/api/tacticalrmm/tacticalrmm/celery.py +++ b/api/tacticalrmm/tacticalrmm/celery.py @@ -56,6 +56,10 @@ "task": "core.tasks.sync_scheduled_tasks", "schedule": crontab(minute="*/2", hour="*"), }, + "sync-mesh-perms-task": { + "task": "core.tasks.sync_mesh_perms_task", + "schedule": crontab(minute="*/4", hour="*"), + }, "resolve-pending-actions": { "task": "core.tasks.resolve_pending_actions", "schedule": timedelta(seconds=100.0), diff --git a/api/tacticalrmm/tacticalrmm/constants.py b/api/tacticalrmm/tacticalrmm/constants.py index 751b129846..4d952af768 100644 --- a/api/tacticalrmm/tacticalrmm/constants.py +++ b/api/tacticalrmm/tacticalrmm/constants.py @@ -30,6 +30,7 @@ def __str__(self): SYNC_SCHED_TASK_LOCK = "sync-sched-tasks-lock-key" AGENT_OUTAGES_LOCK = "agent-outages-task-lock-key" ORPHANED_WIN_TASK_LOCK = "orphaned-win-task-lock-key" +SYNC_MESH_PERMS_TASK_LOCK = "sync-mesh-perms-lock-key" class GoArch(models.TextChoices): diff --git a/api/tacticalrmm/tacticalrmm/logger.py b/api/tacticalrmm/tacticalrmm/logger.py new file mode 100644 index 0000000000..1e15bd044f --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/logger.py @@ -0,0 +1,3 @@ +import logging + +logger = logging.getLogger("trmm") diff --git a/api/tacticalrmm/tacticalrmm/permissions.py b/api/tacticalrmm/tacticalrmm/permissions.py index 9c1399b1c9..0ade57dc07 100644 --- a/api/tacticalrmm/tacticalrmm/permissions.py +++ b/api/tacticalrmm/tacticalrmm/permissions.py @@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404 from agents.models import Agent +from tacticalrmm.constants import AGENT_DEFER if TYPE_CHECKING: from accounts.models import User @@ -33,7 +34,10 @@ def _has_perm_on_agent(user: "User", agent_id: str) -> bool: elif not role: return False - agent = get_object_or_404(Agent, agent_id=agent_id) + agent = get_object_or_404( + Agent.objects.defer(*AGENT_DEFER).select_related("site__client"), + agent_id=agent_id, + ) can_view_clients = role.can_view_clients.all() if role else None can_view_sites = role.can_view_sites.all() if role else None diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index 4e0456cd7d..b6cb08046c 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -1,4 +1,5 @@ import os +import sys from contextlib import suppress from datetime import timedelta from pathlib import Path @@ -114,6 +115,7 @@ SWAGGER_ENABLED = False REDIS_HOST = "127.0.0.1" TRMM_LOG_LEVEL = "ERROR" +TRMM_LOG_TO = "file" with suppress(ImportError): from .local_settings import * # noqa @@ -283,6 +285,24 @@ def get_log_level() -> str: return TRMM_LOG_LEVEL +def configure_logging_handler(): + cfg = { + "level": get_log_level(), + "formatter": "verbose", + } + + log_to = os.getenv("TRMM_LOG_TO", TRMM_LOG_TO) + + if log_to == "stdout": + cfg["class"] = "logging.StreamHandler" + cfg["stream"] = sys.stdout + else: + cfg["class"] = "logging.FileHandler" + cfg["filename"] = os.path.join(LOG_DIR, "trmm_debug.log") + + return cfg + + LOGGING = { "version": 1, "disable_existing_loggers": False, @@ -299,12 +319,7 @@ def get_log_level() -> str: "filename": os.path.join(LOG_DIR, "django_debug.log"), "formatter": "verbose", }, - "trmm": { - "level": get_log_level(), - "class": "logging.FileHandler", - "filename": os.path.join(LOG_DIR, "trmm_debug.log"), - "formatter": "verbose", - }, + "trmm": configure_logging_handler(), }, "loggers": { "django.request": {"handlers": ["file"], "level": "ERROR", "propagate": True},