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:
parent
aedc25c717
commit
3247e19ba8
@ -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 */
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
@ -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')]
|
@ -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"]
|
||||
|
@ -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"),
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user