mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-23 07:56:44 -06:00
Add support for the bulk deletion of restricted objects via REST API
This commit is contained in:
parent
9eb1ce7791
commit
210b8d274f
@ -24,6 +24,7 @@ from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
|
|||||||
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
|
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
|
||||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
|
from utilities.query_functions import CollateAsChar
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from . import serializers
|
from . import serializers
|
||||||
@ -505,6 +506,10 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
|||||||
filterset_class = filtersets.InterfaceFilterSet
|
filterset_class = filtersets.InterfaceFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
def get_bulk_destroy_queryset(self):
|
||||||
|
# Ensure child interfaces are deleted prior to their parents
|
||||||
|
return self.get_queryset().order_by('device', 'parent', CollateAsChar('_name'))
|
||||||
|
|
||||||
|
|
||||||
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||||
queryset = FrontPort.objects.prefetch_related(
|
queryset = FrontPort.objects.prefetch_related(
|
||||||
|
@ -1607,6 +1607,33 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def test_bulk_delete_child_interfaces(self):
|
||||||
|
interface1 = Interface.objects.get(name='Interface 1')
|
||||||
|
device = interface1.device
|
||||||
|
self.add_permissions('dcim.delete_interface')
|
||||||
|
|
||||||
|
# Create a child interface
|
||||||
|
child = Interface.objects.create(
|
||||||
|
device=device,
|
||||||
|
name='Interface 1A',
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL,
|
||||||
|
parent=interface1
|
||||||
|
)
|
||||||
|
self.assertEqual(device.interfaces.count(), 4)
|
||||||
|
|
||||||
|
# Attempt to delete only the parent interface
|
||||||
|
url = self._get_detail_url(interface1)
|
||||||
|
self.client.delete(url, **self.header)
|
||||||
|
self.assertEqual(device.interfaces.count(), 4) # Parent was not deleted
|
||||||
|
|
||||||
|
# Attempt to bulk delete parent & child together
|
||||||
|
data = [
|
||||||
|
{"id": interface1.pk},
|
||||||
|
{"id": child.pk},
|
||||||
|
]
|
||||||
|
self.client.delete(self._get_list_url(), data, format='json', **self.header)
|
||||||
|
self.assertEqual(device.interfaces.count(), 2) # Child & parent were both deleted
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
|
@ -2,7 +2,7 @@ import logging
|
|||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import ProtectedError
|
from django.db.models import ProtectedError, RestrictedError
|
||||||
from django_pglocks import advisory_lock
|
from django_pglocks import advisory_lock
|
||||||
from netbox.constants import ADVISORY_LOCK_KEYS
|
from netbox.constants import ADVISORY_LOCK_KEYS
|
||||||
from rest_framework import mixins as drf_mixins
|
from rest_framework import mixins as drf_mixins
|
||||||
@ -91,8 +91,11 @@ class NetBoxModelViewSet(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
except ProtectedError as e:
|
except (ProtectedError, RestrictedError) as e:
|
||||||
|
if type(e) is ProtectedError:
|
||||||
protected_objects = list(e.protected_objects)
|
protected_objects = list(e.protected_objects)
|
||||||
|
else:
|
||||||
|
protected_objects = list(e.restricted_objects)
|
||||||
msg = f'Unable to delete object. {len(protected_objects)} dependent objects were found: '
|
msg = f'Unable to delete object. {len(protected_objects)} dependent objects were found: '
|
||||||
msg += ', '.join([f'{obj} ({obj.pk})' for obj in protected_objects])
|
msg += ', '.join([f'{obj} ({obj.pk})' for obj in protected_objects])
|
||||||
logger.warning(msg)
|
logger.warning(msg)
|
||||||
|
@ -137,11 +137,14 @@ class BulkUpdateModelMixin:
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
def get_bulk_update_queryset(self):
|
||||||
|
return self.get_queryset()
|
||||||
|
|
||||||
def bulk_update(self, request, *args, **kwargs):
|
def bulk_update(self, request, *args, **kwargs):
|
||||||
partial = kwargs.pop('partial', False)
|
partial = kwargs.pop('partial', False)
|
||||||
serializer = BulkOperationSerializer(data=request.data, many=True)
|
serializer = BulkOperationSerializer(data=request.data, many=True)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
qs = self.get_queryset().filter(
|
qs = self.get_bulk_update_queryset().filter(
|
||||||
pk__in=[o['id'] for o in serializer.data]
|
pk__in=[o['id'] for o in serializer.data]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -184,10 +187,13 @@ class BulkDestroyModelMixin:
|
|||||||
{"id": 456}
|
{"id": 456}
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
def get_bulk_destroy_queryset(self):
|
||||||
|
return self.get_queryset()
|
||||||
|
|
||||||
def bulk_destroy(self, request, *args, **kwargs):
|
def bulk_destroy(self, request, *args, **kwargs):
|
||||||
serializer = BulkOperationSerializer(data=request.data, many=True)
|
serializer = BulkOperationSerializer(data=request.data, many=True)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
qs = self.get_queryset().filter(
|
qs = self.get_bulk_destroy_queryset().filter(
|
||||||
pk__in=[o['id'] for o in serializer.data]
|
pk__in=[o['id'] for o in serializer.data]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ from rest_framework.routers import APIRootView
|
|||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.api.mixins import ConfigContextQuerySetMixin
|
from extras.api.mixins import ConfigContextQuerySetMixin
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
|
from utilities.query_functions import CollateAsChar
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from virtualization import filtersets
|
from virtualization import filtersets
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
@ -87,3 +88,7 @@ class VMInterfaceViewSet(NetBoxModelViewSet):
|
|||||||
serializer_class = serializers.VMInterfaceSerializer
|
serializer_class = serializers.VMInterfaceSerializer
|
||||||
filterset_class = filtersets.VMInterfaceFilterSet
|
filterset_class = filtersets.VMInterfaceFilterSet
|
||||||
brief_prefetch_fields = ['virtual_machine']
|
brief_prefetch_fields = ['virtual_machine']
|
||||||
|
|
||||||
|
def get_bulk_destroy_queryset(self):
|
||||||
|
# Ensure child interfaces are deleted prior to their parents
|
||||||
|
return self.get_queryset().order_by('virtual_machine', 'parent', CollateAsChar('_name'))
|
||||||
|
@ -293,3 +293,29 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'vrf': vrfs[2].pk,
|
'vrf': vrfs[2].pk,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def test_bulk_delete_child_interfaces(self):
|
||||||
|
interface1 = VMInterface.objects.get(name='Interface 1')
|
||||||
|
virtual_machine = interface1.virtual_machine
|
||||||
|
self.add_permissions('virtualization.delete_vminterface')
|
||||||
|
|
||||||
|
# Create a child interface
|
||||||
|
child = VMInterface.objects.create(
|
||||||
|
virtual_machine=virtual_machine,
|
||||||
|
name='Interface 1A',
|
||||||
|
parent=interface1
|
||||||
|
)
|
||||||
|
self.assertEqual(virtual_machine.interfaces.count(), 4)
|
||||||
|
|
||||||
|
# Attempt to delete only the parent interface
|
||||||
|
url = self._get_detail_url(interface1)
|
||||||
|
self.client.delete(url, **self.header)
|
||||||
|
self.assertEqual(virtual_machine.interfaces.count(), 4) # Parent was not deleted
|
||||||
|
|
||||||
|
# Attempt to bulk delete parent & child together
|
||||||
|
data = [
|
||||||
|
{"id": interface1.pk},
|
||||||
|
{"id": child.pk},
|
||||||
|
]
|
||||||
|
self.client.delete(self._get_list_url(), data, format='json', **self.header)
|
||||||
|
self.assertEqual(virtual_machine.interfaces.count(), 2) # Child & parent were both deleted
|
||||||
|
Loading…
Reference in New Issue
Block a user