From 3cd0a930dcda09d933c2b9c7cfb4a8a32115b2fb Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 5 Jun 2025 09:02:16 -0700 Subject: [PATCH 1/6] 19644 set atomic transactions to appropriate database --- netbox/circuits/views.py | 4 ++-- netbox/dcim/utils.py | 4 ++-- netbox/dcim/views.py | 6 +++--- netbox/ipam/api/views.py | 4 ++-- netbox/netbox/api/viewsets/__init__.py | 6 +++--- netbox/netbox/api/viewsets/mixins.py | 6 +++--- netbox/netbox/views/generic/bulk_views.py | 14 +++++++------- netbox/netbox/views/generic/feature_views.py | 4 ++-- netbox/netbox/views/generic/object_views.py | 4 ++-- netbox/virtualization/views.py | 6 +++--- 10 files changed, 29 insertions(+), 29 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 62056cfbe64..f824858ed90 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -1,5 +1,5 @@ from django.contrib import messages -from django.db import transaction +from django.db import transaction, router from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import gettext_lazy as _ @@ -384,7 +384,7 @@ def post(self, request, pk): if termination_a and termination_z: # Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint - with transaction.atomic(): + with transaction.atomic(router.db_for_write(CircuitTermination)): termination_a.term_side = '_' termination_a.save() termination_z.term_side = 'A' diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 0931761bf02..47948e43f9f 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -1,6 +1,6 @@ from django.apps import apps from django.contrib.contenttypes.models import ContentType -from django.db import transaction +from django.db import transaction, router def compile_path_node(ct_id, object_id): @@ -53,7 +53,7 @@ def rebuild_paths(terminations): for obj in terminations: cable_paths = CablePath.objects.filter(_nodes__contains=obj) - with transaction.atomic(): + with transaction.atomic(router.db_for_write(CablePath)): for cp in cable_paths: cp.delete() create_cablepath(cp.origins) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 98223e3b0ab..5923c80ff2c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,7 +1,7 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.paginator import EmptyPage, PageNotAnInteger -from django.db import transaction +from django.db import transaction, router from django.db.models import Prefetch from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory from django.shortcuts import get_object_or_404, redirect, render @@ -124,7 +124,7 @@ def post(self, request): if form.is_valid(): - with transaction.atomic(): + with transaction.atomic(router.db_for_write(self.queryset.model)): count = 0 cable_ids = set() for obj in self.queryset.filter(pk__in=form.cleaned_data['pk']): @@ -3746,7 +3746,7 @@ def post(self, request, pk): if vc_form.is_valid() and formset.is_valid(): - with transaction.atomic(): + with transaction.atomic(router.db_for_write(Device)): # Save the VirtualChassis vc_form.save() diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index dadbc2e2e09..5507ed2be52 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -2,7 +2,7 @@ from django.contrib.contenttypes.prefetch import GenericPrefetch from django.core.exceptions import ObjectDoesNotExist, PermissionDenied -from django.db import transaction +from django.db import transaction, router from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from django_pglocks import advisory_lock @@ -295,7 +295,7 @@ def post(self, request, pk): # Create the new IP address(es) try: - with transaction.atomic(): + with transaction.atomic(router.db_for_write(serializer.Meta.model)): created = serializer.save() self._validate_objects(created) except ObjectDoesNotExist: diff --git a/netbox/netbox/api/viewsets/__init__.py b/netbox/netbox/api/viewsets/__init__.py index 6495102392d..4c1acebed46 100644 --- a/netbox/netbox/api/viewsets/__init__.py +++ b/netbox/netbox/api/viewsets/__init__.py @@ -2,7 +2,7 @@ from functools import cached_property from django.core.exceptions import ObjectDoesNotExist, PermissionDenied -from django.db import transaction +from django.db import transaction, router from django.db.models import ProtectedError, RestrictedError from django_pglocks import advisory_lock from netbox.constants import ADVISORY_LOCK_KEYS @@ -170,7 +170,7 @@ def perform_create(self, serializer): # Enforce object-level permissions on save() try: - with transaction.atomic(): + with transaction.atomic(router.db_for_write(model)): instance = serializer.save() self._validate_objects(instance) except ObjectDoesNotExist: @@ -190,7 +190,7 @@ def perform_update(self, serializer): # Enforce object-level permissions on save() try: - with transaction.atomic(): + with transaction.atomic(router.db_for_write(model)): instance = serializer.save() self._validate_objects(instance) except ObjectDoesNotExist: diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index e21be234846..9d6e75de4e8 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -1,5 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist -from django.db import transaction +from django.db import transaction, router from django.http import Http404 from rest_framework import status from rest_framework.response import Response @@ -113,7 +113,7 @@ def bulk_update(self, request, *args, **kwargs): return Response(data, status=status.HTTP_200_OK) def perform_bulk_update(self, objects, update_data, partial): - with transaction.atomic(): + with transaction.atomic(router.db_for_write(self.queryset.model)): data_list = [] for obj in objects: data = update_data.get(obj.id) @@ -157,7 +157,7 @@ def bulk_destroy(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) def perform_bulk_destroy(self, objects): - with transaction.atomic(): + with transaction.atomic(router.db_for_write(self.queryset.model)): for obj in objects: if hasattr(obj, 'snapshot'): obj.snapshot() diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 4fd23e84c93..02a51f2e707 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -6,7 +6,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRel from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError -from django.db import transaction, IntegrityError +from django.db import transaction, IntegrityError, router from django.db.models import ManyToManyField, ProtectedError, RestrictedError from django.db.models.fields.reverse_related import ManyToManyRel from django.forms import ModelMultipleChoiceField, MultipleHiddenInput @@ -278,7 +278,7 @@ def post(self, request): logger.debug("Form validation was successful") try: - with transaction.atomic(): + with transaction.atomic(router.db_for_write(model)): new_objs = self._create_objects(form, request) # Enforce object-level permissions @@ -501,7 +501,7 @@ def post(self, request): try: # Iterate through data and bind each record to a new model form instance. - with transaction.atomic(): + with transaction.atomic(router.db_for_write(model)): new_objs = self.create_and_update_objects(form, request) # Enforce object-level permissions @@ -681,7 +681,7 @@ def post(self, request, **kwargs): if form.is_valid(): logger.debug("Form validation was successful") try: - with transaction.atomic(): + with transaction.atomic(router.db_for_write(model)): updated_objects = self._update_objects(form, request) # Enforce object-level permissions @@ -778,7 +778,7 @@ def post(self, request): if form.is_valid(): try: - with transaction.atomic(): + with transaction.atomic(router.db_for_write(self.queryset.model)): renamed_pks = self._rename_objects(form, selected_objects) if '_apply' in request.POST: @@ -875,7 +875,7 @@ def post(self, request, **kwargs): queryset = self.queryset.filter(pk__in=pk_list) deleted_count = queryset.count() try: - with transaction.atomic(): + with transaction.atomic(router.db_for_write(model)): for obj in queryset: # Take a snapshot of change-logged models if hasattr(obj, 'snapshot'): @@ -980,7 +980,7 @@ def post(self, request): } try: - with transaction.atomic(): + with transaction.atomic(router.db_for_write(self.queryset.model)): for obj in data['pk']: diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index 9ad14a3d062..f4cc9419030 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -1,7 +1,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.contrib import messages -from django.db import transaction +from django.db import transaction, router from django.db.models import Q from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import gettext_lazy as _ @@ -240,7 +240,7 @@ def post(self, request): data_file__isnull=False ) - with transaction.atomic(): + with transaction.atomic(router.db_for_write(self.queryset.model)): for obj in selected_objects: obj.sync(save=True) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 0db73b7a6eb..38e292846dc 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -282,7 +282,7 @@ def post(self, request, *args, **kwargs): logger.debug("Form validation was successful") try: - with transaction.atomic(): + with transaction.atomic(router.db_for_write(model)): object_created = form.instance.pk is None obj = form.save() @@ -570,7 +570,7 @@ def post(self, request): if not form.errors and not component_form.errors: try: - with transaction.atomic(): + with transaction.atomic(router.db_for_write(self.queryset.model)): # Create the new components new_objs = [] for component_form in new_components: diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index bfb3382fe3f..9c44f4fa9ef 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -1,6 +1,6 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType -from django.db import transaction +from django.db import router, transaction from django.db.models import Prefetch, Sum from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse @@ -297,7 +297,7 @@ def post(self, request, pk): if form.is_valid(): device_pks = form.cleaned_data['devices'] - with transaction.atomic(): + with transaction.atomic(router.db_for_write(self.queryset.model)): # Assign the selected Devices to the Cluster for device in Device.objects.filter(pk__in=device_pks): @@ -332,7 +332,7 @@ def post(self, request, pk): if form.is_valid(): device_pks = form.cleaned_data['pk'] - with transaction.atomic(): + with transaction.atomic(router.db_for_write(self.queryset.model)): # Remove the selected Devices from the Cluster for device in Device.objects.filter(pk__in=device_pks): From caaed888f73c7e7342be3868d929a8c628c12c0c Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 5 Jun 2025 09:09:27 -0700 Subject: [PATCH 2/6] 19644 set atomic transactions for Job Script run --- netbox/extras/jobs.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index d41901dde06..6a3d54da5e2 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -2,10 +2,11 @@ import traceback from contextlib import ExitStack -from django.db import transaction +from django.db import router, transaction from django.utils.translation import gettext as _ from core.signals import clear_events +from dcim.models import Device from extras.models import Script as ScriptModel from netbox.jobs import JobRunner from netbox.registry import registry @@ -39,10 +40,15 @@ def run_script(self, script, request, data, commit): try: try: + # A script can modify multiple models so need to do an atomic lock on + # both the default database (for non ChangeLogged models) and potentially + # any other database (for ChangeLogged models) - choosing Device as + # the model to use as it has ChangeLoggingMixin with transaction.atomic(): - script.output = script.run(data, commit) - if not commit: - raise AbortTransaction() + with transaction.atomic(router.db_for_write(Device)): + script.output = script.run(data, commit) + if not commit: + raise AbortTransaction() except AbortTransaction: script.log_info(message=_("Database changes have been reverted automatically.")) if script.failed: From 78f1e7d6180c0cec9fee471ce2e0cba9736d745c Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 5 Jun 2025 09:31:17 -0700 Subject: [PATCH 3/6] 19644 set atomic transactions to appropriate database --- netbox/circuits/views.py | 2 +- netbox/dcim/utils.py | 2 +- netbox/dcim/views.py | 4 +-- netbox/extras/jobs.py | 2 +- netbox/ipam/api/views.py | 2 +- netbox/netbox/api/viewsets/__init__.py | 4 +-- netbox/netbox/api/viewsets/mixins.py | 30 ++++++++++---------- netbox/netbox/views/generic/bulk_views.py | 12 ++++---- netbox/netbox/views/generic/feature_views.py | 2 +- netbox/netbox/views/generic/object_views.py | 4 +-- netbox/virtualization/views.py | 4 +-- 11 files changed, 34 insertions(+), 34 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index f824858ed90..373180698de 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -384,7 +384,7 @@ def post(self, request, pk): if termination_a and termination_z: # Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint - with transaction.atomic(router.db_for_write(CircuitTermination)): + with transaction.atomic(using=router.db_for_write(CircuitTermination)): termination_a.term_side = '_' termination_a.save() termination_z.term_side = 'A' diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 47948e43f9f..d9ecc79b9c6 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -53,7 +53,7 @@ def rebuild_paths(terminations): for obj in terminations: cable_paths = CablePath.objects.filter(_nodes__contains=obj) - with transaction.atomic(router.db_for_write(CablePath)): + with transaction.atomic(using=router.db_for_write(CablePath)): for cp in cable_paths: cp.delete() create_cablepath(cp.origins) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 5923c80ff2c..bb51370d844 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -124,7 +124,7 @@ def post(self, request): if form.is_valid(): - with transaction.atomic(router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(self.queryset.model)): count = 0 cable_ids = set() for obj in self.queryset.filter(pk__in=form.cleaned_data['pk']): @@ -3746,7 +3746,7 @@ def post(self, request, pk): if vc_form.is_valid() and formset.is_valid(): - with transaction.atomic(router.db_for_write(Device)): + with transaction.atomic(using=router.db_for_write(Device)): # Save the VirtualChassis vc_form.save() diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index 6a3d54da5e2..feead3e820f 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -45,7 +45,7 @@ def run_script(self, script, request, data, commit): # any other database (for ChangeLogged models) - choosing Device as # the model to use as it has ChangeLoggingMixin with transaction.atomic(): - with transaction.atomic(router.db_for_write(Device)): + with transaction.atomic(using=router.db_for_write(Device)): script.output = script.run(data, commit) if not commit: raise AbortTransaction() diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 5507ed2be52..1d5c4867c2f 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -295,7 +295,7 @@ def post(self, request, pk): # Create the new IP address(es) try: - with transaction.atomic(router.db_for_write(serializer.Meta.model)): + with transaction.atomic(using=router.db_for_write(serializer.Meta.model)): created = serializer.save() self._validate_objects(created) except ObjectDoesNotExist: diff --git a/netbox/netbox/api/viewsets/__init__.py b/netbox/netbox/api/viewsets/__init__.py index 4c1acebed46..fcb1128dee5 100644 --- a/netbox/netbox/api/viewsets/__init__.py +++ b/netbox/netbox/api/viewsets/__init__.py @@ -170,7 +170,7 @@ def perform_create(self, serializer): # Enforce object-level permissions on save() try: - with transaction.atomic(router.db_for_write(model)): + with transaction.atomic(using=router.db_for_write(model)): instance = serializer.save() self._validate_objects(instance) except ObjectDoesNotExist: @@ -190,7 +190,7 @@ def perform_update(self, serializer): # Enforce object-level permissions on save() try: - with transaction.atomic(router.db_for_write(model)): + with transaction.atomic(using=router.db_for_write(model)): instance = serializer.save() self._validate_objects(instance) except ObjectDoesNotExist: diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index 9d6e75de4e8..93bfd57ecf5 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -56,22 +56,22 @@ class SequentialBulkCreatesMixin: which depends on the evaluation of existing objects (such as checking for free space within a rack) functions appropriately. """ - @transaction.atomic def create(self, request, *args, **kwargs): - if not isinstance(request.data, list): - # Creating a single object - return super().create(request, *args, **kwargs) - - return_data = [] - for data in request.data: - serializer = self.get_serializer(data=data) - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) - return_data.append(serializer.data) + with transaction.atomic(using=router.db_for_write(self.queryset.model)): + if not isinstance(request.data, list): + # Creating a single object + return super().create(request, *args, **kwargs) + + return_data = [] + for data in request.data: + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + return_data.append(serializer.data) - headers = self.get_success_headers(serializer.data) + headers = self.get_success_headers(serializer.data) - return Response(return_data, status=status.HTTP_201_CREATED, headers=headers) + return Response(return_data, status=status.HTTP_201_CREATED, headers=headers) class BulkUpdateModelMixin: @@ -113,7 +113,7 @@ def bulk_update(self, request, *args, **kwargs): return Response(data, status=status.HTTP_200_OK) def perform_bulk_update(self, objects, update_data, partial): - with transaction.atomic(router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(self.queryset.model)): data_list = [] for obj in objects: data = update_data.get(obj.id) @@ -157,7 +157,7 @@ def bulk_destroy(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) def perform_bulk_destroy(self, objects): - with transaction.atomic(router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(self.queryset.model)): for obj in objects: if hasattr(obj, 'snapshot'): obj.snapshot() diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 02a51f2e707..3d676a9633e 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -278,7 +278,7 @@ def post(self, request): logger.debug("Form validation was successful") try: - with transaction.atomic(router.db_for_write(model)): + with transaction.atomic(using=router.db_for_write(model)): new_objs = self._create_objects(form, request) # Enforce object-level permissions @@ -501,7 +501,7 @@ def post(self, request): try: # Iterate through data and bind each record to a new model form instance. - with transaction.atomic(router.db_for_write(model)): + with transaction.atomic(using=router.db_for_write(model)): new_objs = self.create_and_update_objects(form, request) # Enforce object-level permissions @@ -681,7 +681,7 @@ def post(self, request, **kwargs): if form.is_valid(): logger.debug("Form validation was successful") try: - with transaction.atomic(router.db_for_write(model)): + with transaction.atomic(using=router.db_for_write(model)): updated_objects = self._update_objects(form, request) # Enforce object-level permissions @@ -778,7 +778,7 @@ def post(self, request): if form.is_valid(): try: - with transaction.atomic(router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(self.queryset.model)): renamed_pks = self._rename_objects(form, selected_objects) if '_apply' in request.POST: @@ -875,7 +875,7 @@ def post(self, request, **kwargs): queryset = self.queryset.filter(pk__in=pk_list) deleted_count = queryset.count() try: - with transaction.atomic(router.db_for_write(model)): + with transaction.atomic(using=router.db_for_write(model)): for obj in queryset: # Take a snapshot of change-logged models if hasattr(obj, 'snapshot'): @@ -980,7 +980,7 @@ def post(self, request): } try: - with transaction.atomic(router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(self.queryset.model)): for obj in data['pk']: diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index f4cc9419030..302b84be0b3 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -240,7 +240,7 @@ def post(self, request): data_file__isnull=False ) - with transaction.atomic(router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(self.queryset.model)): for obj in selected_objects: obj.sync(save=True) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 38e292846dc..a7acbffc069 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -282,7 +282,7 @@ def post(self, request, *args, **kwargs): logger.debug("Form validation was successful") try: - with transaction.atomic(router.db_for_write(model)): + with transaction.atomic(using=router.db_for_write(model)): object_created = form.instance.pk is None obj = form.save() @@ -570,7 +570,7 @@ def post(self, request): if not form.errors and not component_form.errors: try: - with transaction.atomic(router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(self.queryset.model)): # Create the new components new_objs = [] for component_form in new_components: diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 9c44f4fa9ef..cde586eec89 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -297,7 +297,7 @@ def post(self, request, pk): if form.is_valid(): device_pks = form.cleaned_data['devices'] - with transaction.atomic(router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(self.queryset.model)): # Assign the selected Devices to the Cluster for device in Device.objects.filter(pk__in=device_pks): @@ -332,7 +332,7 @@ def post(self, request, pk): if form.is_valid(): device_pks = form.cleaned_data['pk'] - with transaction.atomic(router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(self.queryset.model)): # Remove the selected Devices from the Cluster for device in Device.objects.filter(pk__in=device_pks): From 9e2248cbf15dcf2ef2f42e12f20a3f2adfcdb4aa Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 5 Jun 2025 09:50:18 -0700 Subject: [PATCH 4/6] 19644 set atomic transactions to appropriate database --- netbox/ipam/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 1d5c4867c2f..fe4532112a8 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -295,7 +295,7 @@ def post(self, request, pk): # Create the new IP address(es) try: - with transaction.atomic(using=router.db_for_write(serializer.Meta.model)): + with transaction.atomic(using=router.db_for_write(self.queryset.model)): created = serializer.save() self._validate_objects(created) except ObjectDoesNotExist: From 100f08d04d742d00f811ab69ef8ebbdfb6d3a403 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 20 Jun 2025 09:05:10 -0700 Subject: [PATCH 5/6] 19644 fix review comments --- netbox/circuits/views.py | 2 +- netbox/dcim/utils.py | 2 +- netbox/dcim/views.py | 4 ++-- netbox/extras/jobs.py | 10 ++++------ netbox/ipam/api/views.py | 2 +- netbox/netbox/api/viewsets/__init__.py | 2 +- netbox/netbox/api/viewsets/mixins.py | 2 +- netbox/netbox/views/generic/bulk_views.py | 2 +- netbox/netbox/views/generic/feature_views.py | 2 +- netbox/virtualization/views.py | 4 ++-- 10 files changed, 15 insertions(+), 17 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 373180698de..0b4439857a4 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -1,5 +1,5 @@ from django.contrib import messages -from django.db import transaction, router +from django.db import router, transaction from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import gettext_lazy as _ diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index d9ecc79b9c6..a03790ea29a 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -1,6 +1,6 @@ from django.apps import apps from django.contrib.contenttypes.models import ContentType -from django.db import transaction, router +from django.db import router, transaction def compile_path_node(ct_id, object_id): diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index bb51370d844..304438698ca 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,7 +1,7 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.paginator import EmptyPage, PageNotAnInteger -from django.db import transaction, router +from django.db import router, transaction from django.db.models import Prefetch from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory from django.shortcuts import get_object_or_404, redirect, render @@ -124,7 +124,7 @@ def post(self, request): if form.is_valid(): - with transaction.atomic(using=router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(Cable)): count = 0 cable_ids = set() for obj in self.queryset.filter(pk__in=form.cleaned_data['pk']): diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index feead3e820f..45686b001dd 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -2,11 +2,10 @@ import traceback from contextlib import ExitStack -from django.db import router, transaction +from django.db import transaction from django.utils.translation import gettext as _ from core.signals import clear_events -from dcim.models import Device from extras.models import Script as ScriptModel from netbox.jobs import JobRunner from netbox.registry import registry @@ -45,10 +44,9 @@ def run_script(self, script, request, data, commit): # any other database (for ChangeLogged models) - choosing Device as # the model to use as it has ChangeLoggingMixin with transaction.atomic(): - with transaction.atomic(using=router.db_for_write(Device)): - script.output = script.run(data, commit) - if not commit: - raise AbortTransaction() + script.output = script.run(data, commit) + if not commit: + raise AbortTransaction() except AbortTransaction: script.log_info(message=_("Database changes have been reverted automatically.")) if script.failed: diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index fe4532112a8..b0a7ad408aa 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -2,7 +2,7 @@ from django.contrib.contenttypes.prefetch import GenericPrefetch from django.core.exceptions import ObjectDoesNotExist, PermissionDenied -from django.db import transaction, router +from django.db import router, transaction from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from django_pglocks import advisory_lock diff --git a/netbox/netbox/api/viewsets/__init__.py b/netbox/netbox/api/viewsets/__init__.py index fcb1128dee5..2039f735b63 100644 --- a/netbox/netbox/api/viewsets/__init__.py +++ b/netbox/netbox/api/viewsets/__init__.py @@ -2,7 +2,7 @@ from functools import cached_property from django.core.exceptions import ObjectDoesNotExist, PermissionDenied -from django.db import transaction, router +from django.db import router, transaction from django.db.models import ProtectedError, RestrictedError from django_pglocks import advisory_lock from netbox.constants import ADVISORY_LOCK_KEYS diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index 93bfd57ecf5..4fedebad5df 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -1,5 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist -from django.db import transaction, router +from django.db import router, transaction from django.http import Http404 from rest_framework import status from rest_framework.response import Response diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 3d676a9633e..b52d12d9869 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -6,7 +6,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRel from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError -from django.db import transaction, IntegrityError, router +from django.db import IntegrityError, router, transaction from django.db.models import ManyToManyField, ProtectedError, RestrictedError from django.db.models.fields.reverse_related import ManyToManyRel from django.forms import ModelMultipleChoiceField, MultipleHiddenInput diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index 302b84be0b3..d8ba2b4757d 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -1,7 +1,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.contrib import messages -from django.db import transaction, router +from django.db import router, transaction from django.db.models import Q from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import gettext_lazy as _ diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index cde586eec89..6013a56f485 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -297,7 +297,7 @@ def post(self, request, pk): if form.is_valid(): device_pks = form.cleaned_data['devices'] - with transaction.atomic(using=router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(Device)): # Assign the selected Devices to the Cluster for device in Device.objects.filter(pk__in=device_pks): @@ -332,7 +332,7 @@ def post(self, request, pk): if form.is_valid(): device_pks = form.cleaned_data['pk'] - with transaction.atomic(using=router.db_for_write(self.queryset.model)): + with transaction.atomic(using=router.db_for_write(Device)): # Remove the selected Devices from the Cluster for device in Device.objects.filter(pk__in=device_pks): From 3b4ca5ee482b0fcb33acd9cbd2bbfcb4c21fa790 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 24 Jun 2025 10:06:05 -0700 Subject: [PATCH 6/6] 19644 fix review comments --- netbox/extras/jobs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index 45686b001dd..73365419851 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -41,8 +41,7 @@ def run_script(self, script, request, data, commit): try: # A script can modify multiple models so need to do an atomic lock on # both the default database (for non ChangeLogged models) and potentially - # any other database (for ChangeLogged models) - choosing Device as - # the model to use as it has ChangeLoggingMixin + # any other database (for ChangeLogged models) with transaction.atomic(): script.output = script.run(data, commit) if not commit: