diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 62056cfbe..f824858ed 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 @@ class CircuitSwapTerminations(generic.ObjectEditView): 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 0931761bf..47948e43f 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 98223e3b0..5923c80ff 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 @@ class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View) 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 @@ class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, V 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 dadbc2e2e..5507ed2be 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -2,7 +2,7 @@ from copy import deepcopy 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 @@ class AvailableObjectsView(ObjectValidationMixin, APIView): # 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 649510239..4c1acebed 100644 --- a/netbox/netbox/api/viewsets/__init__.py +++ b/netbox/netbox/api/viewsets/__init__.py @@ -2,7 +2,7 @@ import logging 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 @@ class NetBoxModelViewSet( # 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 @@ class NetBoxModelViewSet( # 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 e21be2348..9d6e75de4 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 @@ class BulkUpdateModelMixin: 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 @@ class BulkDestroyModelMixin: 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 4fd23e84c..02a51f2e7 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -6,7 +6,7 @@ from django.contrib import messages 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 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView): 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 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): 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 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView): 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 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView): 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 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView): 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 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): } 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 9ad14a3d0..f4cc94190 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 @@ class BulkSyncDataView(GetReturnURLMixin, BaseMultiObjectView): 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 0db73b7a6..38e292846 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -282,7 +282,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): 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 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): 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 bfb3382fe..9c44f4fa9 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 @@ class ClusterAddDevicesView(generic.ObjectEditView): 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 @@ class ClusterRemoveDevicesView(generic.ObjectEditView): 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):