Files
tuskar-ui/openstack_dashboard/dashboards/project/volumes/forms.py
Josh Durgin 3247e19ba8 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>
2013-05-13 16:00:03 -07:00

345 lines
16 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nebula, Inc.
# All rights reserved.
"""
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
class CreateForm(forms.SelfHandlingForm):
name = forms.CharField(max_length="255", label=_("Volume Name"))
description = forms.CharField(widget=forms.Textarea,
label=_("Description"), required=False)
type = forms.ChoiceField(label=_("Type"),
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'},
data_attrs=('size', 'display_name'),
transform=lambda x:
("%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)
volume_types = cinder.volume_type_list(request)
self.fields['type'].choices = [("", "")] + \
[(type.name, type.name)
for type in volume_types]
# Hide the volume encryption field if the hypervisor doesn't support it
# NOTE: as of Grizzly this is not yet supported in Nova so enabling
# this setting will not do anything useful
hypervisor_features = getattr(settings,
"OPENSTACK_HYPERVISOR_FEATURES",
{})
can_encrypt_volumes = hypervisor_features.get("can_encrypt_volumes",
False)
if can_encrypt_volumes:
# TODO(laura-glendenning) get from api call in future
encryption_options = {"LUKS": "dmcrypt LUKS"}
self.fields['encryption'].choices = [("", "")] + \
[(enc, display) for enc, display in encryption_options.items()]
else:
self.fields['encryption'].widget = forms.widgets.HiddenInput()
self.fields['encryption'].required = False
if ("snapshot_id" in request.GET):
try:
snapshot = self.get_snapshot(request,
request.GET["snapshot_id"])
self.fields['name'].initial = snapshot.display_name
self.fields['size'].initial = snapshot.size
self.fields['snapshot_source'].choices = ((snapshot.id,
snapshot),)
try:
# Set the volume type from the original volume
orig_volume = cinder.volume_get(request,
snapshot.volume_id)
self.fields['type'].initial = orig_volume.volume_type
except:
pass
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
else:
del self.fields['snapshot_source']
except:
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
# error message when the quota is exceeded when trying to create
# a volume, so we need to check for that scenario here before we
# send it off to try and create.
usages = quotas.tenant_quota_usages(request)
snapshot_id = 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"])
snapshot_id = snapshot.id
if (data['size'] < snapshot.size):
error_message = _('The volume size cannot be less than '
'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'])
if usages['gigabytes']['available'] < data['size']:
error_message = _('A volume of %(req)iGB cannot be created as '
'you only have %(avail)iGB of your quota '
'available.')
params = {'req': data['size'],
'avail': usages['gigabytes']['available']}
raise ValidationError(error_message % params)
elif usages['volumes']['available'] <= 0:
error_message = _('You are already using all of your available'
' volumes.')
raise ValidationError(error_message)
metadata = {}
if data['encryption']:
metadata['encryption'] = data['encryption']
volume = cinder.volume_create(request,
data['size'],
data['name'],
data['description'],
data['type'],
snapshot_id=snapshot_id,
image_id=image_id,
metadata=metadata)
message = 'Creating volume "%s"' % data['name']
messages.info(request, message)
return volume
except ValidationError, e:
self.api_error(e.messages[0])
return False
except:
exceptions.handle(request, ignore=True)
self.api_error(_("Unable to create volume."))
return False
@memoized
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"),
help_text=_("Select an instance to "
"attach to."))
device = forms.CharField(label=_("Device Name"))
def __init__(self, *args, **kwargs):
super(AttachForm, self).__init__(*args, **kwargs)
# Hide the device field if the hypervisor doesn't support it.
hypervisor_features = getattr(settings,
"OPENSTACK_HYPERVISOR_FEATURES",
{})
can_set_mount_point = hypervisor_features.get("can_set_mount_point",
True)
if not can_set_mount_point:
self.fields['device'].widget = forms.widgets.HiddenInput()
self.fields['device'].required = False
# populate volume_id
volume = kwargs.get('initial', {}).get("volume", None)
if volume:
volume_id = volume.id
else:
volume_id = None
self.fields['volume_id'] = forms.CharField(widget=forms.HiddenInput(),
initial=volume_id)
# Populate instance choices
instance_list = kwargs.get('initial', {}).get('instances', [])
instances = []
for instance in instance_list:
if instance.status in ACTIVE_STATES and \
not any(instance.id == att["server_id"]
for att in volume.attachments):
instances.append((instance.id, '%s (%s)' % (instance.name,
instance.id)))
if instances:
instances.insert(0, ("", _("Select an instance")))
else:
instances = (("", _("No instances available")),)
self.fields['instance'].choices = instances
def handle(self, request, data):
instance_choices = dict(self.fields['instance'].choices)
instance_name = instance_choices.get(data['instance'],
_("Unknown instance (None)"))
# The name of the instance in the choices list has the ID appended to
# it, so let's slice that off...
instance_name = instance_name.rsplit(" (")[0]
try:
attach = api.nova.instance_volume_attach(request,
data['volume_id'],
data['instance'],
data.get('device', ''))
volume = cinder.volume_get(request, data['volume_id'])
if not volume.display_name:
volume_name = volume.id
else:
volume_name = volume.display_name
message = _('Attaching volume %(vol)s to instance '
'%(inst)s on %(dev)s.') % {"vol": volume_name,
"inst": instance_name,
"dev": attach.device}
messages.info(request, message)
return True
except:
redirect = reverse("horizon:project:volumes:index")
exceptions.handle(request,
_('Unable to attach volume.'),
redirect=redirect)
class CreateSnapshotForm(forms.SelfHandlingForm):
name = forms.CharField(max_length="255", label=_("Snapshot Name"))
description = forms.CharField(widget=forms.Textarea,
label=_("Description"), required=False)
def __init__(self, request, *args, **kwargs):
super(CreateSnapshotForm, self).__init__(request, *args, **kwargs)
# populate volume_id
volume_id = kwargs.get('initial', {}).get('volume_id', [])
self.fields['volume_id'] = forms.CharField(widget=forms.HiddenInput(),
initial=volume_id)
def handle(self, request, data):
try:
snapshot = cinder.volume_snapshot_create(request,
data['volume_id'],
data['name'],
data['description'])
message = _('Creating volume snapshot "%s"') % data['name']
messages.info(request, message)
return snapshot
except:
redirect = reverse("horizon:project:images_and_snapshots:index")
exceptions.handle(request,
_('Unable to create volume snapshot.'),
redirect=redirect)