diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py index 69b489b3a9..0507d690c5 100644 --- a/openstack_dashboard/api/cinder.py +++ b/openstack_dashboard/api/cinder.py @@ -370,7 +370,9 @@ def volume_get(request, volume_id): @profiler.trace def volume_create(request, size, name, description, volume_type, snapshot_id=None, metadata=None, image_id=None, - availability_zone=None, source_volid=None): + availability_zone=None, source_volid=None, + group_id=None): + client = _cinderclient_with_generic_groups(request) data = {'name': name, 'description': description, 'volume_type': volume_type, @@ -378,9 +380,10 @@ def volume_create(request, size, name, description, volume_type, 'metadata': metadata, 'imageRef': image_id, 'availability_zone': availability_zone, - 'source_volid': source_volid} + 'source_volid': source_volid, + 'group_id': group_id} - volume = cinderclient(request).volumes.create(size, **data) + volume = client.volumes.create(size, **data) return Volume(volume) diff --git a/openstack_dashboard/dashboards/project/volumes/forms.py b/openstack_dashboard/dashboards/project/volumes/forms.py index 0a1e099397..b59c2cc8f0 100644 --- a/openstack_dashboard/dashboards/project/volumes/forms.py +++ b/openstack_dashboard/dashboards/project/volumes/forms.py @@ -17,6 +17,8 @@ Views for managing volumes. """ +from cinderclient import exceptions as cinder_exc + from django.conf import settings from django.forms import ValidationError from django.template.defaultfilters import filesizeformat @@ -123,6 +125,10 @@ class CreateForm(forms.SelfHandlingForm): 'data-switch-on': 'source', 'data-source-no_source_type': _('Availability Zone'), 'data-source-image_source': _('Availability Zone')})) + group = forms.ThemableChoiceField( + label=_("Group"), required=False, + help_text=_("Group which the new volume belongs to. Choose " + "'No group' if the new volume belongs to no group.")) def prepare_source_fields_if_snapshot_specified(self, request): try: @@ -261,6 +267,21 @@ class CreateForm(forms.SelfHandlingForm): else: del self.fields['volume_source_type'] + def _populate_group_choices(self, request): + try: + groups = cinder.group_list(request) + except cinder_exc.VersionNotFoundForAPIMethod: + del self.fields['group'] + return + except Exception: + redirect = reverse("horizon:project:volumes:index") + exceptions.handle(request, + _('Unable to retrieve the volume group list.'), + redirect=redirect) + group_choices = [(g.id, g.name or g.id) for g in groups] + group_choices.insert(0, ("", _("No group"))) + self.fields['group'].choices = group_choices + def __init__(self, request, *args, **kwargs): super(CreateForm, self).__init__(request, *args, **kwargs) volume_types = [] @@ -287,6 +308,8 @@ class CreateForm(forms.SelfHandlingForm): else: self.prepare_source_fields_default(request) + self._populate_group_choices(request) + def clean(self): cleaned_data = super(CreateForm, self).clean() source_type = self.cleaned_data.get('volume_source_type') @@ -400,7 +423,8 @@ class CreateForm(forms.SelfHandlingForm): image_id=image_id, metadata=metadata, availability_zone=az, - source_volid=volume_id) + source_volid=volume_id, + group_id=data.get('group') or None) message = _('Creating volume "%s"') % data['name'] messages.info(request, message) return volume diff --git a/openstack_dashboard/dashboards/project/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/tests.py index 7e14e2130f..928e2eeaa9 100644 --- a/openstack_dashboard/dashboards/project/volumes/tests.py +++ b/openstack_dashboard/dashboards/project/volumes/tests.py @@ -220,7 +220,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): cinder: ['volume_create', 'volume_snapshot_list', 'volume_type_list', 'volume_type_default', 'volume_list', 'availability_zone_list', - 'extension_supported'], + 'extension_supported', 'group_list'], quotas: ['tenant_quota_usages'], api.glance: ['image_list_detailed'], }) @@ -250,6 +250,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_extension_supported.return_value = True self.mock_volume_list.return_value = self.cinder_volumes.list() self.mock_volume_create.return_value = volume + self.mock_group_list.return_value = [] url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) @@ -270,7 +271,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_volume_create.assert_called_once_with( test.IsHttpRequest(), formData['size'], formData['name'], formData['description'], formData['type'], metadata={}, - snapshot_id=None, image_id=None, + snapshot_id=None, group_id=None, image_id=None, availability_zone=formData['availability_zone'], source_volid=None) self.mock_image_list_detailed.assert_called_with( test.IsHttpRequest(), @@ -278,6 +279,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_tenant_quota_usages.assert_called_once_with( test.IsHttpRequest(), targets=('volumes', 'gigabytes')) + self.mock_group_list.assert_called_with(test.IsHttpRequest()) @test.create_mocks({ quotas: ['tenant_quota_usages'], @@ -288,7 +290,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'volume_type_default', 'volume_type_list', 'volume_snapshot_list', - 'volume_create'], + 'volume_create', + 'group_list'], }) def test_create_volume_without_name(self): volume = self.cinder_volumes.first() @@ -318,6 +321,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.cinder_volume_types.first() self.mock_volume_list.return_value = self.cinder_volumes.list() self.mock_volume_create.return_value = volume + self.mock_group_list.return_value = [] url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) @@ -326,7 +330,6 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.assertRedirectsNoFollow(res, redirect_url) self.mock_volume_type_list.assert_called_once() - self.mock_tenant_quota_usages.assert_called_once() self.mock_volume_snapshot_list.assert_called_once_with( test.IsHttpRequest(), search_opts=SEARCH_OPTS) self.mock_image_list_detailed.assert_called_with( @@ -340,8 +343,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_volume_create.assert_called_once_with( test.IsHttpRequest(), formData['size'], formData['name'], formData['description'], formData['type'], metadata={}, - snapshot_id=None, image_id=None, + snapshot_id=None, group_id=None, image_id=None, availability_zone=formData['availability_zone'], source_volid=None) + self.mock_group_list.assert_called_once_with(test.IsHttpRequest()) @test.create_mocks({ quotas: ['tenant_quota_usages'], @@ -352,7 +356,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'volume_type_default', 'volume_type_list', 'volume_snapshot_list', - 'volume_create'], + 'volume_create', + 'group_list'], }) def test_create_volume_dropdown(self): volume = self.cinder_volumes.first() @@ -380,6 +385,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_extension_supported.return_value = True self.mock_availability_zone_list.return_value = \ self.cinder_availability_zones.list() + self.mock_group_list.return_value = [] self.mock_volume_create.return_value = volume @@ -405,7 +411,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_volume_create.assert_called_once_with( test.IsHttpRequest(), formData['size'], formData['name'], formData['description'], '', metadata={}, snapshot_id=None, - image_id=None, availability_zone=None, source_volid=None) + group_id=None, image_id=None, availability_zone=None, + source_volid=None) + self.mock_group_list.assert_called_with(test.IsHttpRequest()) @test.create_mocks({ quotas: ['tenant_quota_usages'], @@ -413,7 +421,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'volume_type_default', 'volume_get', 'volume_snapshot_get', - 'volume_create'], + 'volume_create', + 'group_list'], }) def test_create_volume_from_snapshot(self): volume = self.cinder_volumes.first() @@ -434,6 +443,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_volume_snapshot_get.return_value = snapshot self.mock_volume_get.return_value = self.cinder_volumes.first() self.mock_volume_create.return_value = volume + self.mock_group_list.return_value = [] # get snapshot from url url = reverse('horizon:project:volumes:create') @@ -454,7 +464,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_volume_create.assert_called_once_with( test.IsHttpRequest(), formData['size'], formData['name'], formData['description'], '', metadata={}, snapshot_id=snapshot.id, - image_id=None, availability_zone=None, source_volid=None) + group_id=None, image_id=None, availability_zone=None, + source_volid=None) + self.mock_group_list.assert_called_once_with(test.IsHttpRequest()) @test.create_mocks({ quotas: ['tenant_quota_usages'], @@ -467,7 +479,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'volume_list', 'volume_type_default', 'volume_get', - 'volume_create'], + 'volume_create', + 'group_list'], }) def test_create_volume_from_volume(self): volume = self.cinder_volumes.first() @@ -497,6 +510,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_image_list_detailed.return_value = \ [self.images.list(), False, False] self.mock_volume_create.return_value = volume + self.mock_group_list.return_value = [] url = reverse('horizon:project:volumes:create') redirect_url = INDEX_URL @@ -523,7 +537,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_volume_create.assert_called_once_with( test.IsHttpRequest(), formData['size'], formData['name'], formData['description'], '', metadata={}, snapshot_id=None, - image_id=None, availability_zone=None, source_volid=volume.id) + group_id=None, image_id=None, availability_zone=None, + source_volid=volume.id) + self.mock_group_list.assert_called_once_with(test.IsHttpRequest()) @test.create_mocks({ quotas: ['tenant_quota_usages'], @@ -536,7 +552,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'volume_get', 'volume_snapshot_get', 'volume_snapshot_list', - 'volume_create'], + 'volume_create', + 'group_list'], }) def test_create_volume_from_snapshot_dropdown(self): volume = self.cinder_volumes.first() @@ -565,6 +582,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_availability_zone_list.return_value = \ self.cinder_availability_zones.list() self.mock_volume_create.return_value = volume + self.mock_group_list.return_value = [] # get snapshot from dropdown list url = reverse('horizon:project:volumes:create') @@ -591,7 +609,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_volume_create.assert_called_once_with( test.IsHttpRequest(), formData['size'], formData['name'], formData['description'], '', metadata={}, snapshot_id=snapshot.id, - image_id=None, availability_zone=None, source_volid=None) + group_id=None, image_id=None, availability_zone=None, + source_volid=None) + self.mock_group_list.assert_called_once_with(test.IsHttpRequest()) @test.create_mocks({ quotas: ['tenant_quota_usages'], @@ -599,7 +619,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): cinder: ['volume_snapshot_get', 'volume_type_list', 'volume_type_default', - 'volume_get'], + 'volume_get', + 'group_list'], }) def test_create_volume_from_snapshot_invalid_size(self): snapshot = self.cinder_volume_snapshots.first() @@ -616,6 +637,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.cinder_quota_usages.first() self.mock_volume_snapshot_get.return_value = snapshot self.mock_volume_get.return_value = self.cinder_volumes.first() + self.mock_group_list.return_value = [] url = reverse('horizon:project:volumes:create') res = self.client.post("?".join([url, @@ -632,6 +654,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): str(snapshot.id)) self.mock_volume_get.assert_called_with(test.IsHttpRequest(), snapshot.volume_id) + self.mock_group_list.assert_called_with(test.IsHttpRequest()) @test.create_mocks({ quotas: ['tenant_quota_usages'], @@ -640,7 +663,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'availability_zone_list', 'volume_type_default', 'volume_type_list', - 'volume_create'], + 'volume_create', + 'group_list'], }) def test_create_volume_from_image(self): volume = self.cinder_volumes.first() @@ -661,6 +685,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_extension_supported.return_value = True self.mock_availability_zone_list.return_value = \ self.cinder_availability_zones.list() + self.mock_group_list.return_value = [] self.mock_volume_create.return_value = volume @@ -684,7 +709,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_volume_create.assert_called_once_with( test.IsHttpRequest(), formData['size'], formData['name'], formData['description'], '', metadata={}, snapshot_id=None, - image_id=image.id, availability_zone=None, source_volid=None) + group_id=None, image_id=image.id, availability_zone=None, + source_volid=None) + self.mock_group_list.assert_called_with(test.IsHttpRequest()) @test.create_mocks({ quotas: ['tenant_quota_usages'], @@ -696,7 +723,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'volume_list', 'volume_type_list', 'volume_type_default', - 'volume_create'], + 'volume_create', + 'group_list'], }) def test_create_volume_from_image_dropdown(self): volume = self.cinder_volumes.first() @@ -725,6 +753,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_extension_supported.return_value = True self.mock_availability_zone_list.return_value = \ self.cinder_availability_zones.list() + self.mock_group_list.return_value = [] self.mock_volume_create.return_value = volume @@ -753,7 +782,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_volume_create.assert_called_once_with( test.IsHttpRequest(), formData['size'], formData['name'], formData['description'], '', metadata={}, snapshot_id=None, - image_id=image.id, availability_zone=None, source_volid=None) + group_id=None, image_id=image.id, availability_zone=None, + source_volid=None) + self.mock_group_list.assert_called_with(test.IsHttpRequest()) @test.create_mocks({ quotas: ['tenant_quota_usages'], @@ -761,7 +792,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): cinder: ['extension_supported', 'availability_zone_list', 'volume_type_list', - 'volume_type_default'], + 'volume_type_default', + 'group_list'], }) def test_create_volume_from_image_under_image_size(self): image = self.images.first() @@ -778,6 +810,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.cinder_quota_usages.first() self.mock_image_get.return_value = image self.mock_extension_supported.return_value = True + self.mock_group_list.return_value = [] url = reverse('horizon:project:volumes:create') res = self.client.post("?".join([url, @@ -798,6 +831,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): str(image.id)) self.mock_extension_supported.assert_called_with(test.IsHttpRequest(), 'AvailabilityZones') + self.mock_group_list.assert_called_with(test.IsHttpRequest()) @test.create_mocks({ quotas: ['tenant_quota_usages'], @@ -805,7 +839,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): cinder: ['extension_supported', 'availability_zone_list', 'volume_type_list', - 'volume_type_default'], + 'volume_type_default', + 'group_list'], }) def _test_create_volume_from_image_under_image_min_disk_size(self, image): formData = {'name': u'A Volume I Am Making', @@ -823,6 +858,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_extension_supported.return_value = True self.mock_availability_zone_list.return_value = \ self.cinder_availability_zones.list() + self.mock_group_list.return_value = [] url = reverse('horizon:project:volumes:create') res = self.client.post("?".join([url, @@ -840,6 +876,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): str(image.id)) self.mock_extension_supported.assert_called_with(test.IsHttpRequest(), 'AvailabilityZones') + self.mock_group_list.assert_called_with(test.IsHttpRequest()) def test_create_volume_from_image_under_image_min_disk_size(self): image = self.images.get(name="protected_images") @@ -865,7 +902,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'volume_list', 'volume_type_list', 'volume_type_default', - 'volume_snapshot_list'], + 'volume_snapshot_list', + 'group_list'], }) def test_create_volume_gb_used_over_alloted_quota(self): formData = {'name': u'This Volume Is Huge!', @@ -892,6 +930,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_extension_supported.return_value = True self.mock_availability_zone_list.return_value = \ self.cinder_availability_zones.list() + self.mock_group_list.return_value = [] url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) @@ -913,6 +952,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): filters={'visibility': 'shared', 'status': 'active'}) self.mock_extension_supported.assert_called_with(test.IsHttpRequest(), 'AvailabilityZones') + self.mock_group_list.assert_called_with(test.IsHttpRequest()) @test.create_mocks({ quotas: ['tenant_quota_usages'], @@ -922,7 +962,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): 'volume_list', 'volume_type_list', 'volume_type_default', - 'volume_snapshot_list'], + 'volume_snapshot_list', + 'group_list'], }) def test_create_volume_number_over_alloted_quota(self): formData = {'name': u'Too Many...', @@ -950,6 +991,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.mock_extension_supported.return_value = True self.mock_availability_zone_list.return_value = \ self.cinder_availability_zones.list() + self.mock_group_list.return_value = [] url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) @@ -971,6 +1013,74 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): search_opts=SEARCH_OPTS) self.mock_extension_supported.assert_called_with(test.IsHttpRequest(), 'AvailabilityZones') + self.mock_group_list.assert_called_with(test.IsHttpRequest()) + + @test.create_mocks({ + cinder: ['volume_create', 'volume_snapshot_list', + 'volume_type_list', 'volume_type_default', + 'volume_list', 'availability_zone_list', + 'extension_supported', 'group_list'], + quotas: ['tenant_quota_usages'], + api.glance: ['image_list_detailed'], + }) + def test_create_volume_with_group(self): + volume = self.cinder_volumes.first() + volume_type = self.cinder_volume_types.first() + az = self.cinder_availability_zones.first().zoneName + volume_group = self.cinder_groups.list()[0] + formData = {'name': u'A Volume I Am Making', + 'description': u'This is a volume I am making for a test.', + 'method': u'CreateForm', + 'type': volume_type.name, + 'size': 50, + 'snapshot_source': '', + 'availability_zone': az, + 'group': volume_group.id} + + self.mock_volume_type_default.return_value = \ + self.cinder_volume_types.first() + self.mock_volume_type_list.return_value = \ + self.cinder_volume_types.list() + self.mock_tenant_quota_usages.return_value = \ + self.cinder_quota_usages.first() + self.mock_volume_snapshot_list.return_value = \ + self.cinder_volume_snapshots.list() + self.mock_image_list_detailed.return_value = [[], False, False] + self.mock_availability_zone_list.return_value = \ + self.cinder_availability_zones.list() + self.mock_extension_supported.return_value = True + self.mock_volume_list.return_value = self.cinder_volumes.list() + self.mock_volume_create.return_value = volume + self.mock_group_list.return_value = self.cinder_groups.list() + + url = reverse('horizon:project:volumes:create') + res = self.client.post(url, formData) + self.assertNoFormErrors(res) + + redirect_url = INDEX_URL + self.assertRedirectsNoFollow(res, redirect_url) + + self.mock_volume_type_default.assert_called_once() + self.mock_volume_type_list.assert_called_once() + self.mock_volume_snapshot_list.assert_called_once_with( + test.IsHttpRequest(), search_opts=SEARCH_OPTS) + self.mock_availability_zone_list.assert_called_once() + self.mock_extension_supported.assert_called_once_with( + test.IsHttpRequest(), 'AvailabilityZones') + self.mock_volume_list.assert_called_once_with(test.IsHttpRequest(), + search_opts=SEARCH_OPTS) + self.mock_volume_create.assert_called_once_with( + test.IsHttpRequest(), formData['size'], formData['name'], + formData['description'], formData['type'], metadata={}, + snapshot_id=None, group_id=volume_group.id, image_id=None, + availability_zone=formData['availability_zone'], source_volid=None) + self.mock_image_list_detailed.assert_called_with( + test.IsHttpRequest(), + filters={'visibility': 'shared', 'status': 'active'}) + self.mock_tenant_quota_usages.assert_called_once_with( + test.IsHttpRequest(), + targets=('volumes', 'gigabytes')) + self.mock_group_list.assert_called_with(test.IsHttpRequest()) @test.create_mocks({ api.nova: ['server_list'],