Skip to content

Commit

Permalink
Restrict deletion of used cloud flavors and providers
Browse files Browse the repository at this point in the history
  • Loading branch information
romcheg committed Apr 27, 2020
1 parent 721efd2 commit ee76ae2
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 1 deletion.
10 changes: 10 additions & 0 deletions src/ralph/lib/api/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import status
from rest_framework.exceptions import APIException


class Conflict(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = _(
'State of the resource is in conflict with the request.'
)
42 changes: 41 additions & 1 deletion src/ralph/virtual/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
from django.db import transaction
from django.db.models import Prefetch
from rest_framework import relations, serializers
from django.utils.translation import ugettext_lazy as _
from rest_framework import relations, serializers, status
from rest_framework.response import Response

from ralph.api import RalphAPISerializer, RalphAPIViewSet, router
from ralph.api.serializers import RalphAPISaveSerializer
Expand All @@ -20,6 +22,7 @@
from ralph.configuration_management.api import SCMInfoSerializer
from ralph.data_center.api.serializers import DataCenterAssetSimpleSerializer
from ralph.data_center.models import DCHost
from ralph.lib.api.exceptions import Conflict
from ralph.security.api import SecurityScanSerializer
from ralph.security.models import SecurityScan
from ralph.virtual.admin import VirtualServerAdmin
Expand Down Expand Up @@ -180,11 +183,48 @@ class CloudFlavorViewSet(RalphAPIViewSet):
prefetch_related = ['tags', 'virtualcomponent_set__model']
filter_fields = ['flavor_id']

def destroy(self, request, *args, **kwargs):
instance = self.get_object()
force_delete = request.data.get('force', False)

if instance.cloudhost_set.count() != 0 and not force_delete:
raise Conflict(
_(
"Cloud flavor is in use and hence is not delible. "
"Use {\"force\": true} to force deletion."
)
)

self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)


class CloudProviderViewSet(RalphAPIViewSet):
queryset = CloudProvider.objects.all()
serializer_class = CloudProviderSerializer

def _require_force_delete(self, cloud_provider):
return (
cloud_provider.cloudflavor_set.count() != 0 or
cloud_provider.cloudhost_set.count() != 0 or
cloud_provider.cloudproject_set.count() != 0
)

def destroy(self, request, *args, **kwargs):
instance = self.get_object()
force_delete = request.data.get('force', False)

if self._require_force_delete(instance) and not force_delete:
raise Conflict(
_(
"Cloud provider is in use and hence is not delible. "
"Use {\"force\": true} to force deletion."
)
)

self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)


class CloudImageViewSet(RalphAPIViewSet):
queryset = CloudImage.objects.all()
Expand Down
108 changes: 108 additions & 0 deletions src/ralph/virtual/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,114 @@ def test_patch_cloudprovider(self):
provider = CloudProvider.objects.get(name=args['name'])
self.assertEqual(provider.name, args['name'])

def test_delete_cloud_flavor_returns_409_if_is_used_by_cloud_hosts(self):
# given
cloud_flavor = CloudFlavorFactory()
CloudHostFactory(cloudflavor=cloud_flavor)

# when
url = reverse('cloudflavor-detail', args=(cloud_flavor.pk,))
resp = self.client.delete(url)

# then
self.assertEqual(resp.status_code, status.HTTP_409_CONFLICT)
self.assertIn(
'Cloud flavor is in use and hence is not delible.',
resp.data['detail']
)
self.assertTrue(
CloudFlavor.objects.filter(pk=cloud_flavor.pk).exists()
)

def test_unused_cloud_flavor_can_be_deleted(self):
# given
cloud_flavor = CloudFlavorFactory()

# when
url = reverse('cloudflavor-detail', args=(cloud_flavor.pk,))
resp = self.client.delete(url)

# then
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
self.assertRaises(
CloudFlavor.DoesNotExist,
cloud_flavor.refresh_from_db
)

def test_used_cloud_flavor_can_be_deleted_with_force(self):
# given
cloud_flavor = CloudFlavorFactory()
CloudHostFactory(cloudflavor=cloud_flavor)

# when
url = reverse('cloudflavor-detail', args=(cloud_flavor.pk,))
data = {'force': True}
resp = self.client.delete(url, data=data)

# then
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
self.assertRaises(
CloudFlavor.DoesNotExist,
cloud_flavor.refresh_from_db
)

@data(CloudFlavorFactory, CloudHostFactory, CloudProjectFactory)
def test_delete_cloud_provider_returns_409_if_has_child_objects(
self, child_type
):
# given
cloud_provider = CloudProviderFactory(name="test-cloud-provider")
child_type(cloudprovider=cloud_provider)

# when
url = reverse('cloudprovider-detail', args=(cloud_provider.pk,))
resp = self.client.delete(url)

# then
self.assertEqual(resp.status_code, status.HTTP_409_CONFLICT)
self.assertIn(
'Cloud provider is in use and hence is not delible.',
resp.data['detail']
)
self.assertTrue(
CloudProvider.objects.filter(pk=cloud_provider.pk).exists()
)

def test_empty_cloud_provider_can_be_deleted(self):
# given
cloud_provider = CloudProviderFactory(name="test-cloud-provider")

# when
url = reverse('cloudprovider-detail', args=(cloud_provider.pk,))
resp = self.client.delete(url)

# then
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
self.assertRaises(
CloudProvider.DoesNotExist,
cloud_provider.refresh_from_db
)

@data(CloudFlavorFactory, CloudHostFactory, CloudProjectFactory)
def test_non_empty_cloud_provider_can_be_deleted_with_force(
self, child_type
):
# given
cloud_provider = CloudProviderFactory(name="test-cloud-provider")
child_type(cloudprovider=cloud_provider)

# when
url = reverse('cloudprovider-detail', args=(cloud_provider.pk,))
data = {'force': True}
resp = self.client.delete(url, data=data)

# then
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
self.assertRaises(
CloudProvider.DoesNotExist,
cloud_provider.refresh_from_db
)

def test_inheritance_of_service_env_on_change_in_a_cloud_project(self):
url = reverse('cloudproject-detail', args=(self.cloud_project.id,))
args = {
Expand Down

0 comments on commit ee76ae2

Please sign in to comment.