diff --git a/horizon/static/horizon/js/horizon.forms.js b/horizon/static/horizon/js/horizon.forms.js index ac8f3555b..2147e6f0f 100644 --- a/horizon/static/horizon/js/horizon.forms.js +++ b/horizon/static/horizon/js/horizon.forms.js @@ -9,6 +9,16 @@ horizon.forms = { var $volSize = $form.find('input#id_size'); $volSize.val($option.data("size")); }); + }, + handle_image_source: function() { + $("div.table_wrapper, #modal_wrapper").on("change", "select#id_image_source", function(evt) { + var $option = $(this).find("option:selected"); + var $form = $(this).closest('form'); + var $volName = $form.find('input#id_name'); + $volName.val($option.data("name")); + var $volSize = $form.find('input#id_size'); + $volSize.val($option.data("size")); + }); } }; @@ -67,6 +77,7 @@ horizon.addInitFunction(function () { horizon.modals.addModalInitFunction(horizon.forms.init_examples); horizon.forms.handle_snapshot_source(); + horizon.forms.handle_image_source(); // Bind event handlers to confirm dangerous actions. $("body").on("click", "form button.btn-danger", function (evt) { @@ -108,6 +119,28 @@ horizon.addInitFunction(function () { $(modal).find('select.switchable').trigger('change'); }); + // Handle field toggles for the Create Volume source type field + function update_volume_source_displayed_fields (field) { + var $this = $(field), + base_type = $this.val(); + + $this.find("option").each(function () { + if (this.value != base_type) { + $("#id_" + this.value).closest(".control-group").hide(); + } else { + $("#id_" + this.value).closest(".control-group").show(); + } + }); + } + + $(document).on('change', '#id_volume_source_type', function (evt) { + update_volume_source_displayed_fields(this); + }); + + $('#id_volume_source_type').change(); + horizon.modals.addModalInitFunction(function (modal) { + $(modal).find("#id_volume_source_type").change(); + }); /* Help tooltips */ diff --git a/horizon/utils/functions.py b/horizon/utils/functions.py index 798a6e388..e7ae8d468 100644 --- a/horizon/utils/functions.py +++ b/horizon/utils/functions.py @@ -1,3 +1,5 @@ +import math + from django.utils.encoding import force_unicode from django.utils.functional import lazy @@ -7,3 +9,9 @@ def _lazy_join(separator, strings): for s in strings]) lazy_join = lazy(_lazy_join, unicode) + + +def bytes_to_gigabytes(bytes): + # Converts the number of bytes to the next highest number of Gigabytes + # For example 5000000 (5 Meg) would return '1' + return int(math.ceil(float(bytes) / 1024 ** 3)) diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py index 7f3ebdb79..2917c0774 100644 --- a/openstack_dashboard/api/cinder.py +++ b/openstack_dashboard/api/cinder.py @@ -89,10 +89,10 @@ def volume_get(request, volume_id): def volume_create(request, size, name, description, volume_type, - snapshot_id=None, metadata=None): + snapshot_id=None, metadata=None, image_id=None): return cinderclient(request).volumes.create(size, display_name=name, display_description=description, volume_type=volume_type, - snapshot_id=snapshot_id, metadata=metadata) + snapshot_id=snapshot_id, metadata=metadata, imageRef=image_id) def volume_delete(request, volume_id): diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/images/tables.py b/openstack_dashboard/dashboards/project/images_and_snapshots/images/tables.py index 0fd771a37..7ec89a429 100644 --- a/openstack_dashboard/dashboards/project/images_and_snapshots/images/tables.py +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/images/tables.py @@ -84,6 +84,23 @@ class EditImage(tables.LinkAction): return False +class CreateVolumeFromImage(tables.LinkAction): + name = "create_volume_from_image" + verbose_name = _("Create Volume") + url = "horizon:project:volumes:create" + classes = ("ajax-modal", "btn-camera") + + def get_link_url(self, datum): + base_url = reverse(self.url) + params = urlencode({"image_id": self.table.get_object_id(datum)}) + return "?".join([base_url, params]) + + def allowed(self, request, image=None): + if image: + return image.status == "active" + return False + + def filter_tenants(): return getattr(settings, 'IMAGES_LIST_FILTER_TENANTS', []) @@ -199,5 +216,6 @@ class ImagesTable(tables.DataTable): # all the columns by default. columns = ["name", "status", "public", "protected", "disk_format"] table_actions = (OwnerFilter, CreateImage, DeleteImage,) - row_actions = (LaunchImage, EditImage, DeleteImage,) + row_actions = (LaunchImage, CreateVolumeFromImage, + EditImage, DeleteImage,) pagination_param = "image_marker" diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/utils.py b/openstack_dashboard/dashboards/project/images_and_snapshots/utils.py new file mode 100644 index 000000000..534005a46 --- /dev/null +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/utils.py @@ -0,0 +1,63 @@ +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions + +from openstack_dashboard.api import glance + + +def get_available_images(request, project_id=None, images_cache=None): + """ + Returns a list of images that are public or owned by the given + project_id. If project_id is not specified, only public images + are returned. + + :param images_cache: + An optional dict-like object in which to + cache public and per-project id image metadata. + """ + if images_cache is None: + images_cache = {} + public_images = images_cache.get('public_images', []) + images_by_project = images_cache.get('images_by_project', {}) + if 'public_images' not in images_cache: + public = {"is_public": True, + "status": "active"} + try: + images, _more = glance.image_list_detailed( + request, filters=public) + [public_images.append(image) for image in images] + images_cache['public_images'] = public_images + except: + exceptions.handle(request, + _("Unable to retrieve public images.")) + + # Preempt if we don't have a project_id yet. + if project_id is None: + images_by_project[project_id] = [] + + if project_id not in images_by_project: + owner = {"property-owner_id": project_id, + "status": "active"} + try: + owned_images, _more = glance.image_list_detailed( + request, filters=owner) + except: + exceptions.handle(request, + _("Unable to retrieve images for " + "the current project.")) + images_by_project[project_id] = owned_images + if 'images_by_project' not in images_cache: + images_cache['images_by_project'] = images_by_project + + owned_images = images_by_project[project_id] + images = owned_images + public_images + + # Remove duplicate images + image_ids = [] + final_images = [] + for image in images: + if image.id not in image_ids: + image_ids.append(image.id) + final_images.append(image) + return [image for image in final_images + if image.container_format not in ('aki', 'ari')] diff --git a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py index 357f14c51..54e9215ba 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py +++ b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py @@ -30,8 +30,8 @@ from horizon import workflows from openstack_dashboard import api from openstack_dashboard.api import cinder -from openstack_dashboard.api import glance from openstack_dashboard.usage import quotas +from ...images_and_snapshots.utils import get_available_images LOG = logging.getLogger(__name__) @@ -219,52 +219,14 @@ class SetInstanceDetailsAction(workflows.Action): return cleaned_data - def _get_available_images(self, request, context): - project_id = context.get('project_id', None) - if not hasattr(self, "_public_images"): - public = {"is_public": True, - "status": "active"} - try: - public_images, _more = glance.image_list_detailed( - request, filters=public) - except: - public_images = [] - exceptions.handle(request, - _("Unable to retrieve public images.")) - self._public_images = public_images - - # Preempt if we don't have a project_id yet. - if project_id is None: - setattr(self, "_images_for_%s" % project_id, []) - - if not hasattr(self, "_images_for_%s" % project_id): - owner = {"property-owner_id": project_id, - "status": "active"} - try: - owned_images, _more = glance.image_list_detailed( - request, filters=owner) - except: - owned_images = [] - exceptions.handle(request, - _("Unable to retrieve images for " - "the current project.")) - setattr(self, "_images_for_%s" % project_id, owned_images) - - owned_images = getattr(self, "_images_for_%s" % project_id) - images = owned_images + self._public_images - - # Remove duplicate images - image_ids = [] - final_images = [] - for image in images: - if image.id not in image_ids: - image_ids.append(image.id) - final_images.append(image) - return [image for image in final_images - if image.container_format not in ('aki', 'ari')] + def _init_images_cache(self): + if not hasattr(self, '_images_cache'): + self._images_cache = {} def populate_image_id_choices(self, request, context): - images = self._get_available_images(request, context) + self._init_images_cache() + images = get_available_images(request, context.get('project_id'), + self._images_cache) choices = [(image.id, image.name) for image in images if image.properties.get("image_type", '') != "snapshot"] @@ -275,7 +237,9 @@ class SetInstanceDetailsAction(workflows.Action): return choices def populate_instance_snapshot_id_choices(self, request, context): - images = self._get_available_images(request, context) + self._init_images_cache() + images = get_available_images(request, context.get('project_id'), + self._images_cache) choices = [(image.id, image.name) for image in images if image.properties.get("image_type", '') == "snapshot"] diff --git a/openstack_dashboard/dashboards/project/volumes/forms.py b/openstack_dashboard/dashboards/project/volumes/forms.py index 98a5d4336..1acd0e676 100644 --- a/openstack_dashboard/dashboards/project/volumes/forms.py +++ b/openstack_dashboard/dashboards/project/volumes/forms.py @@ -10,17 +10,21 @@ Views for managing volumes. from django.conf import settings from django.core.urlresolvers import reverse from django.forms import ValidationError +from django.template.defaultfilters import filesizeformat from django.utils.translation import ugettext_lazy as _ from horizon import forms from horizon import exceptions from horizon import messages from horizon.utils.fields import SelectWidget +from horizon.utils.functions import bytes_to_gigabytes from horizon.utils.memoized import memoized from openstack_dashboard import api from openstack_dashboard.api import cinder +from openstack_dashboard.api import glance from openstack_dashboard.usage import quotas +from ..images_and_snapshots.utils import get_available_images from ..instances.tables import ACTIVE_STATES @@ -32,6 +36,8 @@ class CreateForm(forms.SelfHandlingForm): required=False) size = forms.IntegerField(min_value=1, label=_("Size (GB)")) encryption = forms.ChoiceField(label=_("Encryption"), required=False) + volume_source_type = forms.ChoiceField(label=_("Volume Source"), + required=False) snapshot_source = forms.ChoiceField(label=_("Use snapshot as a source"), widget=SelectWidget( attrs={'class': 'snapshot-selector'}, @@ -40,6 +46,15 @@ class CreateForm(forms.SelfHandlingForm): ("%s (%sGB)" % (x.display_name, x.size))), required=False) + image_source = forms.ChoiceField(label=_("Use image as a source"), + widget=SelectWidget( + attrs={'class': 'image-selector'}, + data_attrs=('size', 'name'), + transform=lambda x: + ("%s (%s)" % + (x.name, + filesizeformat(x.bytes)))), + required=False) def __init__(self, request, *args, **kwargs): super(CreateForm, self).__init__(request, *args, **kwargs) @@ -84,13 +99,35 @@ class CreateForm(forms.SelfHandlingForm): self.fields['size'].help_text = _('Volume size must be equal ' 'to or greater than the snapshot size (%sGB)' % snapshot.size) + del self.fields['image_source'] + del self.fields['volume_source_type'] except: exceptions.handle(request, _('Unable to load the specified snapshot.')) + elif ('image_id' in request.GET): + try: + image = self.get_image(request, + request.GET["image_id"]) + image.bytes = image.size + self.fields['name'].initial = image.name + self.fields['size'].initial = bytes_to_gigabytes(image.size) + self.fields['image_source'].choices = ((image.id, image),) + self.fields['size'].help_text = _('Volume size must be equal ' + 'to or greater than the image size (%s)' + % filesizeformat(image.size)) + del self.fields['snapshot_source'] + del self.fields['volume_source_type'] + except: + msg = _('Unable to load the specified image. %s') + exceptions.handle(request, msg % request.GET['image_id']) else: + source_type_choices = [] + try: snapshots = cinder.volume_snapshot_list(request) if snapshots: + source_type_choices.append(("snapshot_source", + _("Snapshot"))) choices = [('', _("Choose a snapshot"))] + \ [(s.id, s) for s in snapshots] self.fields['snapshot_source'].choices = choices @@ -100,6 +137,27 @@ class CreateForm(forms.SelfHandlingForm): exceptions.handle(request, _("Unable to retrieve " "volume snapshots.")) + images = get_available_images(request, + request.user.tenant_id) + if images: + source_type_choices.append(("image_source", _("Image"))) + choices = [('', _("Choose an image"))] + for image in images: + image.bytes = image.size + image.size = bytes_to_gigabytes(image.bytes) + choices.append((image.id, image)) + self.fields['image_source'].choices = choices + else: + del self.fields['image_source'] + + if source_type_choices: + choices = ([('no_source_type', + _("No source, empty volume."))] + + source_type_choices) + self.fields['volume_source_type'].choices = choices + else: + del self.fields['volume_source_type'] + def handle(self, request, data): try: # FIXME(johnp): cinderclient currently returns a useless @@ -109,7 +167,10 @@ class CreateForm(forms.SelfHandlingForm): usages = quotas.tenant_quota_usages(request) snapshot_id = None - if (data.get("snapshot_source", None)): + image_id = None + source_type = data.get('volume_source_type', None) + if (data.get("snapshot_source", None) and + source_type in [None, 'snapshot_source']): # Create from Snapshot snapshot = self.get_snapshot(request, data["snapshot_source"]) @@ -119,6 +180,18 @@ class CreateForm(forms.SelfHandlingForm): 'the snapshot size (%sGB)' % snapshot.size) raise ValidationError(error_message) + elif (data.get("image_source", None) and + source_type in [None, 'image_source']): + # Create from Snapshot + image = self.get_image(request, + data["image_source"]) + image_id = image.id + image_size = bytes_to_gigabytes(image.size) + if (data['size'] < image_size): + error_message = _('The volume size cannot be less than ' + 'the image size (%s)' % + filesizeformat(image.size)) + raise ValidationError(error_message) else: if type(data['size']) is str: data['size'] = int(data['size']) @@ -146,6 +219,7 @@ class CreateForm(forms.SelfHandlingForm): data['description'], data['type'], snapshot_id=snapshot_id, + image_id=image_id, metadata=metadata) message = 'Creating volume "%s"' % data['name'] messages.info(request, message) @@ -162,6 +236,10 @@ class CreateForm(forms.SelfHandlingForm): def get_snapshot(self, request, id): return cinder.volume_snapshot_get(request, id) + @memoized + def get_image(self, request, id): + return glance.image_get(request, id) + class AttachForm(forms.SelfHandlingForm): instance = forms.ChoiceField(label=_("Attach to Instance"), diff --git a/openstack_dashboard/dashboards/project/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/tests.py index 6dbde8dc2..bf8f5f973 100644 --- a/openstack_dashboard/dashboards/project/volumes/tests.py +++ b/openstack_dashboard/dashboards/project/volumes/tests.py @@ -35,6 +35,7 @@ class VolumeViewTests(test.TestCase): @test.create_stubs({cinder: ('volume_create', 'volume_snapshot_list', 'volume_type_list',), + api.glance: ('image_list_detailed',), quotas: ('tenant_quota_usages',)}) def test_create_volume(self): volume = self.volumes.first() @@ -52,13 +53,22 @@ class VolumeViewTests(test.TestCase): quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_snapshots.list()) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'is_public': True, + 'status': 'active'}) \ + .AndReturn([self.images.list(), False]) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'property-owner_id': self.tenant.id, + 'status': 'active'}) \ + .AndReturn([[], False]) cinder.volume_create(IsA(http.HttpRequest), formData['size'], formData['name'], formData['description'], formData['type'], metadata={}, - snapshot_id=None).AndReturn(volume) + snapshot_id=None, + image_id=None).AndReturn(volume) self.mox.ReplayAll() @@ -70,6 +80,53 @@ class VolumeViewTests(test.TestCase): @test.create_stubs({cinder: ('volume_create', 'volume_snapshot_list', + 'volume_type_list',), + api.glance: ('image_list_detailed',), + quotas: ('tenant_quota_usages',)}) + def test_create_volume_dropdown(self): + volume = self.volumes.first() + usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} + formData = {'name': u'A Volume I Am Making', + 'description': u'This is a volume I am making for a test.', + 'method': u'CreateForm', + 'size': 50, + 'type': '', + 'volume_source_type': 'no_source_type', + 'snapshot_source': self.volume_snapshots.first().id, + 'image_source': self.images.first().id} + + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) + cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_snapshots.list()) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'is_public': True, + 'status': 'active'}) \ + .AndReturn([self.images.list(), False]) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'property-owner_id': self.tenant.id, + 'status': 'active'}) \ + .AndReturn([[], False]) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + cinder.volume_create(IsA(http.HttpRequest), + formData['size'], + formData['name'], + formData['description'], + '', + metadata={}, + snapshot_id=None, + image_id=None).\ + AndReturn(volume) + + self.mox.ReplayAll() + + url = reverse('horizon:project:volumes:create') + res = self.client.post(url, formData) + + redirect_url = reverse('horizon:project:volumes:index') + self.assertRedirectsNoFollow(res, redirect_url) + + @test.create_stubs({cinder: ('volume_create', 'volume_snapshot_get', 'volume_get', 'volume_type_list',), @@ -85,7 +142,6 @@ class VolumeViewTests(test.TestCase): 'type': '', 'snapshot_source': snapshot.id} - # first call- with url param cinder.volume_type_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_types.list()) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) @@ -99,25 +155,9 @@ class VolumeViewTests(test.TestCase): formData['description'], '', metadata={}, - snapshot_id=snapshot.id).\ + snapshot_id=snapshot.id, + image_id=None).\ AndReturn(volume) - # second call- with dropdown - cinder.volume_type_list(IsA(http.HttpRequest)).\ - AndReturn(self.volume_types.list()) - quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) - cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ - AndReturn(self.volume_snapshots.list()) - cinder.volume_snapshot_get(IsA(http.HttpRequest), - str(snapshot.id)).AndReturn(snapshot) - cinder.volume_create(IsA(http.HttpRequest), - formData['size'], - formData['name'], - formData['description'], - '', - metadata={}, - snapshot_id=snapshot.id).\ - AndReturn(volume) - self.mox.ReplayAll() # get snapshot from url @@ -129,6 +169,52 @@ class VolumeViewTests(test.TestCase): redirect_url = reverse('horizon:project:volumes:index') self.assertRedirectsNoFollow(res, redirect_url) + @test.create_stubs({cinder: ('volume_create', + 'volume_snapshot_list', + 'volume_snapshot_get', + 'volume_get', + 'volume_type_list',), + api.glance: ('image_list_detailed',), + quotas: ('tenant_quota_usages',)}) + def test_create_volume_from_snapshot_dropdown(self): + volume = self.volumes.first() + usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} + snapshot = self.volume_snapshots.first() + formData = {'name': u'A Volume I Am Making', + 'description': u'This is a volume I am making for a test.', + 'method': u'CreateForm', + 'size': 50, + 'type': '', + 'volume_source_type': 'snapshot_source', + 'snapshot_source': snapshot.id} + + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) + cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_snapshots.list()) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'is_public': True, + 'status': 'active'}) \ + .AndReturn([self.images.list(), False]) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'property-owner_id': self.tenant.id, + 'status': 'active'}) \ + .AndReturn([[], False]) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + cinder.volume_snapshot_get(IsA(http.HttpRequest), + str(snapshot.id)).AndReturn(snapshot) + cinder.volume_create(IsA(http.HttpRequest), + formData['size'], + formData['name'], + formData['description'], + '', + metadata={}, + snapshot_id=snapshot.id, + image_id=None).\ + AndReturn(volume) + + self.mox.ReplayAll() + # get snapshot from dropdown list url = reverse('horizon:project:volumes:create') res = self.client.post(url, formData) @@ -139,6 +225,7 @@ class VolumeViewTests(test.TestCase): @test.create_stubs({cinder: ('volume_snapshot_get', 'volume_type_list', 'volume_get',), + api.glance: ('image_list_detailed',), quotas: ('tenant_quota_usages',)}) def test_create_volume_from_snapshot_invalid_size(self): usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} @@ -168,7 +255,132 @@ class VolumeViewTests(test.TestCase): "The volume size cannot be less than the " "snapshot size (40GB)") + @test.create_stubs({cinder: ('volume_create', + 'volume_type_list',), + api.glance: ('image_get',), + quotas: ('tenant_quota_usages',)}) + def test_create_volume_from_image(self): + volume = self.volumes.first() + usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} + image = self.images.first() + formData = {'name': u'A Volume I Am Making', + 'description': u'This is a volume I am making for a test.', + 'method': u'CreateForm', + 'size': 40, + 'type': '', + 'image_source': image.id} + + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + api.glance.image_get(IsA(http.HttpRequest), + str(image.id)).AndReturn(image) + cinder.volume_create(IsA(http.HttpRequest), + formData['size'], + formData['name'], + formData['description'], + '', + metadata={}, + snapshot_id=None, + image_id=image.id).\ + AndReturn(volume) + + self.mox.ReplayAll() + + # get image from url + url = reverse('horizon:project:volumes:create') + res = self.client.post("?".join([url, + "image_id=" + str(image.id)]), + formData) + + redirect_url = reverse('horizon:project:volumes:index') + self.assertRedirectsNoFollow(res, redirect_url) + + @test.create_stubs({cinder: ('volume_create', + 'volume_type_list', + 'volume_snapshot_list',), + api.glance: ('image_get', + 'image_list_detailed'), + quotas: ('tenant_quota_usages',)}) + def test_create_volume_from_image_dropdown(self): + volume = self.volumes.first() + usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} + image = self.images.first() + formData = {'name': u'A Volume I Am Making', + 'description': u'This is a volume I am making for a test.', + 'method': u'CreateForm', + 'size': 30, + 'type': '', + 'volume_source_type': 'image_source', + 'snapshot_source': self.volume_snapshots.first().id, + 'image_source': image.id} + + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) + cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_snapshots.list()) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'is_public': True, + 'status': 'active'}) \ + .AndReturn([self.images.list(), False]) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'property-owner_id': self.tenant.id, + 'status': 'active'}) \ + .AndReturn([[], False]) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + api.glance.image_get(IsA(http.HttpRequest), + str(image.id)).AndReturn(image) + cinder.volume_create(IsA(http.HttpRequest), + formData['size'], + formData['name'], + formData['description'], + '', + metadata={}, + snapshot_id=None, + image_id=image.id).\ + AndReturn(volume) + + self.mox.ReplayAll() + + # get image from dropdown list + url = reverse('horizon:project:volumes:create') + res = self.client.post(url, formData) + + redirect_url = reverse('horizon:project:volumes:index') + self.assertRedirectsNoFollow(res, redirect_url) + + @test.create_stubs({cinder: ('volume_type_list',), + api.glance: ('image_get', + 'image_list_detailed'), + quotas: ('tenant_quota_usages',)}) + def test_create_volume_from_image_invalid_size(self): + usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} + image = self.images.first() + formData = {'name': u'A Volume I Am Making', + 'description': u'This is a volume I am making for a test.', + 'method': u'CreateForm', + 'size': 1, 'image_source': image.id} + + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + api.glance.image_get(IsA(http.HttpRequest), + str(image.id)).AndReturn(image) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + + self.mox.ReplayAll() + + url = reverse('horizon:project:volumes:create') + res = self.client.post("?".join([url, + "image_id=" + str(image.id)]), + formData, follow=True) + self.assertEqual(res.redirect_chain, []) + self.assertFormError(res, 'form', None, + "The volume size cannot be less than the " + "image size (20.0 GB)") + @test.create_stubs({cinder: ('volume_snapshot_list', 'volume_type_list',), + api.glance: ('image_list_detailed',), quotas: ('tenant_quota_usages',)}) def test_create_volume_gb_used_over_alloted_quota(self): usage = {'gigabytes': {'available': 100, 'used': 20}} @@ -182,6 +394,14 @@ class VolumeViewTests(test.TestCase): quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_snapshots.list()) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'is_public': True, + 'status': 'active'}) \ + .AndReturn([self.images.list(), False]) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'property-owner_id': self.tenant.id, + 'status': 'active'}) \ + .AndReturn([[], False]) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) self.mox.ReplayAll() @@ -194,6 +414,7 @@ class VolumeViewTests(test.TestCase): self.assertEqual(res.context['form'].errors['__all__'], expected_error) @test.create_stubs({cinder: ('volume_snapshot_list', 'volume_type_list',), + api.glance: ('image_list_detailed',), quotas: ('tenant_quota_usages',)}) def test_create_volume_number_over_alloted_quota(self): usage = {'gigabytes': {'available': 100, 'used': 20}, @@ -208,6 +429,14 @@ class VolumeViewTests(test.TestCase): quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_snapshots.list()) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'is_public': True, + 'status': 'active'}) \ + .AndReturn([self.images.list(), False]) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'property-owner_id': self.tenant.id, + 'status': 'active'}) \ + .AndReturn([[], False]) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) self.mox.ReplayAll() @@ -222,6 +451,7 @@ class VolumeViewTests(test.TestCase): @test.create_stubs({cinder: ('volume_create', 'volume_snapshot_list', 'volume_type_list',), + api.glance: ('image_list_detailed',), quotas: ('tenant_quota_usages',)}) def test_create_volume_encrypted(self): volume = self.volumes.first() @@ -244,13 +474,22 @@ class VolumeViewTests(test.TestCase): quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_snapshots.list()) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'is_public': True, + 'status': 'active'}) \ + .AndReturn([self.images.list(), False]) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'property-owner_id': self.tenant.id, + 'status': 'active'}) \ + .AndReturn([[], False]) cinder.volume_create(IsA(http.HttpRequest), formData['size'], formData['name'], formData['description'], formData['type'], metadata={'encryption': formData['encryption']}, - snapshot_id=None).AndReturn(volume) + snapshot_id=None, + image_id=None).AndReturn(volume) self.mox.ReplayAll() @@ -263,6 +502,7 @@ class VolumeViewTests(test.TestCase): settings.OPENSTACK_HYPERVISOR_FEATURES['can_encrypt_volumes'] = PREV @test.create_stubs({cinder: ('volume_snapshot_list', 'volume_type_list',), + api.glance: ('image_list_detailed',), quotas: ('tenant_quota_usages',)}) def test_create_volume_cannot_encrypt(self): volume = self.volumes.first() @@ -289,6 +529,14 @@ class VolumeViewTests(test.TestCase): quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_snapshots.list()) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'is_public': True, + 'status': 'active'}) \ + .AndReturn([self.images.list(), False]) + api.glance.image_list_detailed(IsA(http.HttpRequest), + filters={'property-owner_id': self.tenant.id, + 'status': 'active'}) \ + .AndReturn([[], False]) self.mox.ReplayAll() diff --git a/openstack_dashboard/test/test_data/glance_data.py b/openstack_dashboard/test/test_data/glance_data.py index 0b5dc4c15..9ad972b0e 100644 --- a/openstack_dashboard/test/test_data/glance_data.py +++ b/openstack_dashboard/test/test_data/glance_data.py @@ -57,6 +57,7 @@ def data(TEST): image_dict = {'id': '007e7d55-fe1e-4c5c-bf08-44b4a4964822', 'name': 'public_image', 'status': "active", + 'size': 20 * 1024 ** 3, 'owner': TEST.tenant.id, 'container_format': 'novaImage', 'properties': {'image_type': u'image'}, @@ -67,6 +68,7 @@ def data(TEST): image_dict = {'id': 'a001c047-22f8-47d0-80a1-8ec94a9524fe', 'name': 'private_image', 'status': "active", + 'size': 10 * 1024 ** 2, 'owner': TEST.tenant.id, 'container_format': 'aki', 'is_public': False, @@ -77,6 +79,7 @@ def data(TEST): 'name': 'protected_images', 'status': "active", 'owner': TEST.tenant.id, + 'size': 2 * 1024 ** 3, 'container_format': 'novaImage', 'properties': {'image_type': u'image'}, 'is_public': True, @@ -86,6 +89,7 @@ def data(TEST): image_dict = {'id': '278905a6-4b52-4d1e-98f9-8c57bb25ba32', 'name': 'public_image 2', 'status': "active", + 'size': 5 * 1024 ** 3, 'owner': TEST.tenant.id, 'container_format': 'novaImage', 'properties': {'image_type': u'image'}, @@ -96,6 +100,7 @@ def data(TEST): image_dict = {'id': '710a1acf-a3e3-41dd-a32d-5d6b6c86ea10', 'name': 'private_image 2', 'status': "active", + 'size': 30 * 1024 ** 3, 'owner': TEST.tenant.id, 'container_format': 'aki', 'is_public': False, @@ -105,6 +110,7 @@ def data(TEST): image_dict = {'id': '7cd892fd-5652-40f3-a450-547615680132', 'name': 'private_image 3', 'status': "active", + 'size': 2 * 1024 ** 3, 'owner': TEST.tenant.id, 'container_format': 'aki', 'is_public': False, @@ -115,6 +121,7 @@ def data(TEST): image_dict = {'id': 'c8756975-7a3b-4e43-b7f7-433576112849', 'name': 'shared_image 1', 'status': "active", + 'size': 8 * 1024 ** 3, 'owner': 'someothertenant', 'container_format': 'aki', 'is_public': False, @@ -126,6 +133,7 @@ def data(TEST): image_dict = {'id': 'f448704f-0ce5-4d34-8441-11b6581c6619', 'name': 'official_image 1', 'status': "active", + 'size': 2 * 1024 ** 3, 'owner': 'officialtenant', 'container_format': 'aki', 'is_public': True,