Add ability to create a volume from an image

Currently there is no way to get data onto a volume through the
dashboard without taking manual steps in a guest. It's possible to
create a volume from a snapshot, but there's no way to get data onto a
volume to create the snapshot in the first place. Creating a volume
from an image is particularly useful when booting from a volume. This
was implemented in cinder and nova-volume in Folsom. Expose this to
dashboard users as well.

Create a new action for this on the images panel, reusing the 'Create
Volume' form. Also make this possible from the volumes panel, by
adding a 'source type' field to the 'Create Volume' form. This lets
users choose among no source (the default), snapshot, and image, hiding
the lists of snapshots and images when the corresponding source type
is not selected, similar to the 'image type' field in the 'Launch
Instance' workflow.

Use the same infrastructure as creating from a snapshot to update the
size and name based on the image size and name.

Extract the image listing code out of the instance panel, into a
generic utility module. Also add a size conversion function, since
image sizes are reported by glance in bytes, while volumes are
specified in gigabytes.

Change-Id: I8314ab011734d80d5f611b338ed6e2538e6bab7e
Signed-off-by: Josh Durgin <josh.durgin@inktank.com>
This commit is contained in:
Josh Durgin 2013-02-18 14:26:40 -08:00
parent aedc25c717
commit 3247e19ba8
9 changed files with 491 additions and 71 deletions

View File

@ -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 */

View File

@ -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))

View File

@ -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):

View File

@ -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"

View File

@ -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')]

View File

@ -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"]

View File

@ -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"),

View File

@ -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()

View File

@ -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,