
Note that there was state leaking from one of the existing tests that was relied upon in other tests and moving that test in the run order caused the others to fail. All related tests have been altered to not leak state. Change-Id: I972bc5650fa77044de8a027f570cf2cb41febef8 Implements: blueprint reorganise-volumes
820 lines
36 KiB
Python
820 lines
36 KiB
Python
# Copyright 2012 Nebula, Inc.
|
|
# All rights reserved.
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""
|
|
Views for managing volumes.
|
|
"""
|
|
|
|
from django.conf import settings
|
|
from django.core.urlresolvers import reverse
|
|
from django.forms import ValidationError # noqa
|
|
from django import http
|
|
from django.template.defaultfilters import filesizeformat # noqa
|
|
from django.utils.translation import pgettext_lazy
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from horizon import exceptions
|
|
from horizon import forms
|
|
from horizon import messages
|
|
from horizon.utils import functions
|
|
from horizon.utils.memoized import memoized # noqa
|
|
|
|
from openstack_dashboard import api
|
|
from openstack_dashboard.api import cinder
|
|
from openstack_dashboard.api import glance
|
|
from openstack_dashboard.api import nova
|
|
from openstack_dashboard.dashboards.project.images import utils
|
|
from openstack_dashboard.dashboards.project.instances import tables
|
|
from openstack_dashboard.usage import quotas
|
|
|
|
IMAGE_BACKEND_SETTINGS = getattr(settings, 'OPENSTACK_IMAGE_BACKEND', {})
|
|
IMAGE_FORMAT_CHOICES = IMAGE_BACKEND_SETTINGS.get('image_formats', [])
|
|
VALID_DISK_FORMATS = ('raw', 'vmdk', 'vdi', 'qcow2', 'vhd', 'vhdx')
|
|
DEFAULT_CONTAINER_FORMAT = 'bare'
|
|
|
|
|
|
# Determine whether the extension for Cinder AZs is enabled
|
|
def cinder_az_supported(request):
|
|
try:
|
|
return cinder.extension_supported(request, 'AvailabilityZones')
|
|
except Exception:
|
|
exceptions.handle(request, _('Unable to determine if availability '
|
|
'zones extension is supported.'))
|
|
return False
|
|
|
|
|
|
def availability_zones(request):
|
|
zone_list = []
|
|
if cinder_az_supported(request):
|
|
try:
|
|
zones = api.cinder.availability_zone_list(request)
|
|
zone_list = [(zone.zoneName, zone.zoneName)
|
|
for zone in zones if zone.zoneState['available']]
|
|
zone_list.sort()
|
|
except Exception:
|
|
exceptions.handle(request, _('Unable to retrieve availability '
|
|
'zones.'))
|
|
if not zone_list:
|
|
zone_list.insert(0, ("", _("No availability zones found")))
|
|
elif len(zone_list) > 1:
|
|
zone_list.insert(0, ("", _("Any Availability Zone")))
|
|
|
|
return zone_list
|
|
|
|
|
|
class CreateForm(forms.SelfHandlingForm):
|
|
name = forms.CharField(max_length=255, label=_("Volume Name"),
|
|
required=False)
|
|
description = forms.CharField(max_length=255, widget=forms.Textarea(
|
|
attrs={'rows': 4}),
|
|
label=_("Description"), required=False)
|
|
volume_source_type = forms.ChoiceField(
|
|
label=_("Volume Source"),
|
|
required=False,
|
|
widget=forms.ThemableSelectWidget(attrs={
|
|
'class': 'switchable',
|
|
'data-slug': 'source'}))
|
|
snapshot_source = forms.ChoiceField(
|
|
label=_("Use snapshot as a source"),
|
|
widget=forms.ThemableSelectWidget(
|
|
attrs={'class': 'snapshot-selector'},
|
|
data_attrs=('size', 'name'),
|
|
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
|
required=False)
|
|
image_source = forms.ChoiceField(
|
|
label=_("Use image as a source"),
|
|
widget=forms.ThemableSelectWidget(
|
|
attrs={'class': 'image-selector'},
|
|
data_attrs=('size', 'name', 'min_disk'),
|
|
transform=lambda x: "%s (%s)" % (x.name, filesizeformat(x.bytes))),
|
|
required=False)
|
|
volume_source = forms.ChoiceField(
|
|
label=_("Use a volume as source"),
|
|
widget=forms.ThemableSelectWidget(
|
|
attrs={'class': 'image-selector'},
|
|
data_attrs=('size', 'name'),
|
|
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
|
required=False)
|
|
type = forms.ChoiceField(
|
|
label=_("Type"),
|
|
required=False,
|
|
widget=forms.ThemableSelectWidget(
|
|
attrs={'class': 'switched',
|
|
'data-switch-on': 'source',
|
|
'data-source-no_source_type': _('Type'),
|
|
'data-source-image_source': _('Type')}))
|
|
size = forms.IntegerField(min_value=1, initial=1, label=_("Size (GiB)"))
|
|
availability_zone = forms.ChoiceField(
|
|
label=_("Availability Zone"),
|
|
required=False,
|
|
widget=forms.ThemableSelectWidget(
|
|
attrs={'class': 'switched',
|
|
'data-switch-on': 'source',
|
|
'data-source-no_source_type': _('Availability Zone'),
|
|
'data-source-image_source': _('Availability Zone')}))
|
|
|
|
def prepare_source_fields_if_snapshot_specified(self, request):
|
|
try:
|
|
snapshot = self.get_snapshot(request,
|
|
request.GET["snapshot_id"])
|
|
self.fields['name'].initial = snapshot.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 Exception:
|
|
pass
|
|
self.fields['size'].help_text = (
|
|
_('Volume size must be equal to or greater than the '
|
|
'snapshot size (%sGiB)') % snapshot.size)
|
|
self.fields['type'].widget = forms.widgets.HiddenInput()
|
|
del self.fields['image_source']
|
|
del self.fields['volume_source']
|
|
del self.fields['volume_source_type']
|
|
del self.fields['availability_zone']
|
|
|
|
except Exception:
|
|
exceptions.handle(request,
|
|
_('Unable to load the specified snapshot.'))
|
|
|
|
def prepare_source_fields_if_image_specified(self, request):
|
|
self.fields['availability_zone'].choices = \
|
|
availability_zones(request)
|
|
try:
|
|
image = self.get_image(request,
|
|
request.GET["image_id"])
|
|
image.bytes = image.size
|
|
self.fields['name'].initial = image.name
|
|
min_vol_size = functions.bytes_to_gigabytes(
|
|
image.size)
|
|
size_help_text = (_('Volume size must be equal to or greater '
|
|
'than the image size (%s)')
|
|
% filesizeformat(image.size))
|
|
properties = getattr(image, 'properties', {})
|
|
min_disk_size = (getattr(image, 'min_disk', 0) or
|
|
properties.get('min_disk', 0))
|
|
if min_disk_size > min_vol_size:
|
|
min_vol_size = min_disk_size
|
|
size_help_text = (_('Volume size must be equal to or '
|
|
'greater than the image minimum '
|
|
'disk size (%sGiB)')
|
|
% min_disk_size)
|
|
self.fields['size'].initial = min_vol_size
|
|
self.fields['size'].help_text = size_help_text
|
|
self.fields['image_source'].choices = ((image.id, image),)
|
|
del self.fields['snapshot_source']
|
|
del self.fields['volume_source']
|
|
del self.fields['volume_source_type']
|
|
except Exception:
|
|
msg = _('Unable to load the specified image. %s')
|
|
exceptions.handle(request, msg % request.GET['image_id'])
|
|
|
|
def prepare_source_fields_if_volume_specified(self, request):
|
|
self.fields['availability_zone'].choices = \
|
|
availability_zones(request)
|
|
volume = None
|
|
try:
|
|
volume = self.get_volume(request, request.GET["volume_id"])
|
|
except Exception:
|
|
msg = _('Unable to load the specified volume. %s')
|
|
exceptions.handle(request, msg % request.GET['volume_id'])
|
|
|
|
if volume is not None:
|
|
self.fields['name'].initial = volume.name
|
|
self.fields['description'].initial = volume.description
|
|
min_vol_size = volume.size
|
|
size_help_text = (_('Volume size must be equal to or greater '
|
|
'than the origin volume size (%sGiB)')
|
|
% volume.size)
|
|
self.fields['size'].initial = min_vol_size
|
|
self.fields['size'].help_text = size_help_text
|
|
self.fields['volume_source'].choices = ((volume.id, volume),)
|
|
self.fields['type'].initial = volume.type
|
|
del self.fields['snapshot_source']
|
|
del self.fields['image_source']
|
|
del self.fields['volume_source_type']
|
|
|
|
def prepare_source_fields_default(self, request):
|
|
source_type_choices = []
|
|
self.fields['availability_zone'].choices = \
|
|
availability_zones(request)
|
|
|
|
try:
|
|
available = api.cinder.VOLUME_STATE_AVAILABLE
|
|
snapshots = cinder.volume_snapshot_list(
|
|
request, search_opts=dict(status=available))
|
|
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 Exception:
|
|
exceptions.handle(request,
|
|
_("Unable to retrieve volume snapshots."))
|
|
|
|
images = utils.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 = functions.bytes_to_gigabytes(image.bytes)
|
|
choices.append((image.id, image))
|
|
self.fields['image_source'].choices = choices
|
|
else:
|
|
del self.fields['image_source']
|
|
|
|
volumes = self.get_volumes(request)
|
|
if volumes:
|
|
source_type_choices.append(("volume_source", _("Volume")))
|
|
choices = [('', _("Choose a volume"))]
|
|
for volume in volumes:
|
|
choices.append((volume.id, volume))
|
|
self.fields['volume_source'].choices = choices
|
|
else:
|
|
del self.fields['volume_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 __init__(self, request, *args, **kwargs):
|
|
super(CreateForm, self).__init__(request, *args, **kwargs)
|
|
volume_types = []
|
|
try:
|
|
volume_types = cinder.volume_type_list(request)
|
|
except Exception:
|
|
redirect_url = reverse("horizon:project:volumes:index")
|
|
error_message = _('Unable to retrieve the volume type list.')
|
|
exceptions.handle(request, error_message, redirect=redirect_url)
|
|
self.fields['type'].choices = [("", _("No volume type"))] + \
|
|
[(type.name, type.name)
|
|
for type in volume_types]
|
|
if 'initial' in kwargs and 'type' in kwargs['initial']:
|
|
# if there is a default volume type to select, then remove
|
|
# the first ""No volume type" entry
|
|
self.fields['type'].choices.pop(0)
|
|
|
|
if "snapshot_id" in request.GET:
|
|
self.prepare_source_fields_if_snapshot_specified(request)
|
|
elif 'image_id' in request.GET:
|
|
self.prepare_source_fields_if_image_specified(request)
|
|
elif 'volume_id' in request.GET:
|
|
self.prepare_source_fields_if_volume_specified(request)
|
|
else:
|
|
self.prepare_source_fields_default(request)
|
|
|
|
def clean(self):
|
|
cleaned_data = super(CreateForm, self).clean()
|
|
source_type = self.cleaned_data.get('volume_source_type')
|
|
if (source_type == 'image_source' and
|
|
not cleaned_data.get('image_source')):
|
|
msg = _('Image source must be specified')
|
|
self._errors['image_source'] = self.error_class([msg])
|
|
elif (source_type == 'snapshot_source' and
|
|
not cleaned_data.get('snapshot_source')):
|
|
msg = _('Snapshot source must be specified')
|
|
self._errors['snapshot_source'] = self.error_class([msg])
|
|
elif (source_type == 'volume_source' and
|
|
not cleaned_data.get('volume_source')):
|
|
msg = _('Volume source must be specified')
|
|
self._errors['volume_source'] = self.error_class([msg])
|
|
return cleaned_data
|
|
|
|
def get_volumes(self, request):
|
|
volumes = []
|
|
try:
|
|
available = api.cinder.VOLUME_STATE_AVAILABLE
|
|
volumes = cinder.volume_list(self.request,
|
|
search_opts=dict(status=available))
|
|
except Exception:
|
|
exceptions.handle(request,
|
|
_('Unable to retrieve list of volumes.'))
|
|
return volumes
|
|
|
|
def handle(self, request, data):
|
|
try:
|
|
usages = quotas.tenant_limit_usages(self.request)
|
|
availableGB = usages['maxTotalVolumeGigabytes'] - \
|
|
usages['gigabytesUsed']
|
|
availableVol = usages['maxTotalVolumes'] - usages['volumesUsed']
|
|
|
|
snapshot_id = None
|
|
image_id = None
|
|
volume_id = None
|
|
source_type = data.get('volume_source_type', None)
|
|
az = data.get('availability_zone', None) or None
|
|
volume_type = data.get('type')
|
|
|
|
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 (%sGiB)')
|
|
% snapshot.size)
|
|
raise ValidationError(error_message)
|
|
az = None
|
|
volume_type = ""
|
|
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 = functions.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)
|
|
properties = getattr(image, 'properties', {})
|
|
min_disk_size = (getattr(image, 'min_disk', 0) or
|
|
properties.get('min_disk', 0))
|
|
if min_disk_size > 0 and data['size'] < min_disk_size:
|
|
error_message = (_('The volume size cannot be less than '
|
|
'the image minimum disk size (%sGiB)')
|
|
% min_disk_size)
|
|
raise ValidationError(error_message)
|
|
elif (data.get("volume_source", None) and
|
|
source_type in ['', None, 'volume_source']):
|
|
# Create from volume
|
|
volume = self.get_volume(request, data["volume_source"])
|
|
volume_id = volume.id
|
|
|
|
if data['size'] < volume.size:
|
|
error_message = (_('The volume size cannot be less than '
|
|
'the source volume size (%sGiB)')
|
|
% volume.size)
|
|
raise ValidationError(error_message)
|
|
else:
|
|
if type(data['size']) is str:
|
|
data['size'] = int(data['size'])
|
|
|
|
if availableGB < data['size']:
|
|
error_message = _('A volume of %(req)iGiB cannot be created '
|
|
'as you only have %(avail)iGiB of your '
|
|
'quota available.')
|
|
params = {'req': data['size'],
|
|
'avail': availableGB}
|
|
raise ValidationError(error_message % params)
|
|
elif availableVol <= 0:
|
|
error_message = _('You are already using all of your available'
|
|
' volumes.')
|
|
raise ValidationError(error_message)
|
|
|
|
metadata = {}
|
|
|
|
volume = cinder.volume_create(request,
|
|
data['size'],
|
|
data['name'],
|
|
data['description'],
|
|
volume_type,
|
|
snapshot_id=snapshot_id,
|
|
image_id=image_id,
|
|
metadata=metadata,
|
|
availability_zone=az,
|
|
source_volid=volume_id)
|
|
message = _('Creating volume "%s"') % data['name']
|
|
messages.info(request, message)
|
|
return volume
|
|
except ValidationError as e:
|
|
self.api_error(e.messages[0])
|
|
return False
|
|
except Exception:
|
|
redirect = reverse("horizon:project:volumes:index")
|
|
exceptions.handle(request,
|
|
_("Unable to create volume."),
|
|
redirect=redirect)
|
|
|
|
@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)
|
|
|
|
@memoized
|
|
def get_volume(self, request, id):
|
|
return cinder.volume_get(request, id)
|
|
|
|
|
|
class AttachForm(forms.SelfHandlingForm):
|
|
instance = forms.ThemableChoiceField(label=_("Attach to Instance"),
|
|
help_text=_("Select an instance to "
|
|
"attach to."))
|
|
|
|
device = forms.CharField(label=_("Device Name"),
|
|
widget=forms.TextInput(attrs={'placeholder':
|
|
'/dev/vdc'}),
|
|
required=False,
|
|
help_text=_("Actual device name may differ due "
|
|
"to hypervisor settings. If not "
|
|
"specified, then hypervisor will "
|
|
"select a device name."))
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(AttachForm, self).__init__(*args, **kwargs)
|
|
|
|
# Hide the device field if the hypervisor doesn't support it.
|
|
if not nova.can_set_mount_point():
|
|
self.fields['device'].widget = forms.widgets.HiddenInput()
|
|
|
|
# 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 tables.VOLUME_ATTACH_READY_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]
|
|
|
|
# api requires non-empty device name or None
|
|
device = data.get('device') or None
|
|
|
|
try:
|
|
attach = api.nova.instance_volume_attach(request,
|
|
data['volume_id'],
|
|
data['instance'],
|
|
device)
|
|
volume = cinder.volume_get(request, data['volume_id'])
|
|
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 Exception:
|
|
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(max_length=255,
|
|
widget=forms.Textarea(attrs={'rows': 4}),
|
|
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:
|
|
volume = cinder.volume_get(request,
|
|
data['volume_id'])
|
|
force = False
|
|
message = _('Creating volume snapshot "%s".') % data['name']
|
|
if volume.status == 'in-use':
|
|
force = True
|
|
message = _('Forcing to create snapshot "%s" '
|
|
'from attached volume.') % data['name']
|
|
snapshot = cinder.volume_snapshot_create(request,
|
|
data['volume_id'],
|
|
data['name'],
|
|
data['description'],
|
|
force=force)
|
|
|
|
messages.info(request, message)
|
|
return snapshot
|
|
except Exception as e:
|
|
redirect = reverse("horizon:project:volumes:index")
|
|
msg = _('Unable to create volume snapshot.')
|
|
if e.code == 413:
|
|
msg = _('Requested snapshot would exceed the allowed quota.')
|
|
exceptions.handle(request,
|
|
msg,
|
|
redirect=redirect)
|
|
|
|
|
|
class CreateTransferForm(forms.SelfHandlingForm):
|
|
name = forms.CharField(max_length=255, label=_("Transfer Name"))
|
|
|
|
def clean_name(self):
|
|
cleaned_name = self.cleaned_data['name']
|
|
if cleaned_name.isspace():
|
|
msg = _('Volume transfer name cannot be empty.')
|
|
self._errors['name'] = self.error_class([msg])
|
|
|
|
return cleaned_name
|
|
|
|
def handle(self, request, data):
|
|
try:
|
|
volume_id = self.initial['volume_id']
|
|
transfer = cinder.transfer_create(request, volume_id, data['name'])
|
|
|
|
msg = _('Created volume transfer: "%s".') % data['name']
|
|
messages.success(request, msg)
|
|
response = http.HttpResponseRedirect(
|
|
reverse("horizon:project:volumes:show_transfer",
|
|
args=(transfer.id, transfer.auth_key)))
|
|
return response
|
|
except Exception:
|
|
redirect = reverse("horizon:project:volumes:index")
|
|
exceptions.handle(request, _('Unable to create volume transfer.'),
|
|
redirect=redirect)
|
|
|
|
|
|
class AcceptTransferForm(forms.SelfHandlingForm):
|
|
# These max lengths correspond to the sizes in cinder
|
|
transfer_id = forms.CharField(max_length=36, label=_("Transfer ID"))
|
|
auth_key = forms.CharField(max_length=16, label=_("Authorization Key"))
|
|
|
|
def handle(self, request, data):
|
|
try:
|
|
transfer = cinder.transfer_accept(request,
|
|
data['transfer_id'],
|
|
data['auth_key'])
|
|
|
|
msg = (_('Successfully accepted volume transfer: "%s"')
|
|
% data['transfer_id'])
|
|
messages.success(request, msg)
|
|
return transfer
|
|
except Exception:
|
|
redirect = reverse("horizon:project:volumes:index")
|
|
exceptions.handle(request, _('Unable to accept volume transfer.'),
|
|
redirect=redirect)
|
|
|
|
|
|
class ShowTransferForm(forms.SelfHandlingForm):
|
|
name = forms.CharField(
|
|
label=_("Transfer Name"),
|
|
widget=forms.TextInput(attrs={'readonly': 'readonly'}),
|
|
required=False)
|
|
id = forms.CharField(
|
|
label=_("Transfer ID"),
|
|
widget=forms.TextInput(attrs={'readonly': 'readonly'}),
|
|
required=False)
|
|
auth_key = forms.CharField(
|
|
label=_("Authorization Key"),
|
|
widget=forms.TextInput(attrs={'readonly': 'readonly'}),
|
|
required=False)
|
|
|
|
def handle(self, request, data):
|
|
pass
|
|
|
|
|
|
class UpdateForm(forms.SelfHandlingForm):
|
|
name = forms.CharField(max_length=255,
|
|
label=_("Volume Name"),
|
|
required=False)
|
|
description = forms.CharField(max_length=255,
|
|
widget=forms.Textarea(attrs={'rows': 4}),
|
|
label=_("Description"),
|
|
required=False)
|
|
bootable = forms.BooleanField(label=_("Bootable"),
|
|
required=False,
|
|
help_text=_("Specifies that the volume can "
|
|
"be used to launch an instance"))
|
|
|
|
def handle(self, request, data):
|
|
volume_id = self.initial['volume_id']
|
|
try:
|
|
cinder.volume_update(request, volume_id, data['name'],
|
|
data['description'])
|
|
except Exception:
|
|
redirect = reverse("horizon:project:volumes:index")
|
|
exceptions.handle(request,
|
|
_('Unable to update volume.'),
|
|
redirect=redirect)
|
|
|
|
# only update bootable flag if modified
|
|
make_bootable = data['bootable']
|
|
if make_bootable != self.initial['bootable']:
|
|
try:
|
|
cinder.volume_set_bootable(request, volume_id, make_bootable)
|
|
except Exception:
|
|
redirect = reverse("horizon:project:volumes:index")
|
|
exceptions.handle(request,
|
|
_('Unable to set bootable flag on volume.'),
|
|
redirect=redirect)
|
|
|
|
message = _('Updating volume "%s"') % data['name']
|
|
messages.info(request, message)
|
|
return True
|
|
|
|
|
|
class UploadToImageForm(forms.SelfHandlingForm):
|
|
name = forms.CharField(label=_('Volume Name'),
|
|
widget=forms.TextInput(
|
|
attrs={'readonly': 'readonly'}))
|
|
image_name = forms.CharField(max_length=255, label=_('Image Name'))
|
|
disk_format = forms.ChoiceField(label=_('Disk Format'),
|
|
widget=forms.ThemableSelectWidget(),
|
|
required=False)
|
|
force = forms.BooleanField(
|
|
label=pgettext_lazy("Force upload volume in in-use status to image",
|
|
u"Force"),
|
|
widget=forms.CheckboxInput(),
|
|
required=False)
|
|
|
|
def __init__(self, request, *args, **kwargs):
|
|
super(UploadToImageForm, self).__init__(request, *args, **kwargs)
|
|
|
|
# 'vhd','iso','aki','ari' and 'ami' disk formats are supported by
|
|
# glance, but not by qemu-img. qemu-img supports 'vpc', 'cloop', 'cow'
|
|
# and 'qcow' which are not supported by glance.
|
|
# I can only use 'raw', 'vmdk', 'vdi' or 'qcow2' so qemu-img will not
|
|
# have issues when processes image request from cinder.
|
|
disk_format_choices = [(value, name) for value, name
|
|
in IMAGE_FORMAT_CHOICES
|
|
if value in VALID_DISK_FORMATS]
|
|
self.fields['disk_format'].choices = disk_format_choices
|
|
self.fields['disk_format'].initial = 'raw'
|
|
if self.initial['status'] != 'in-use':
|
|
self.fields['force'].widget = forms.widgets.HiddenInput()
|
|
|
|
def handle(self, request, data):
|
|
volume_id = self.initial['id']
|
|
|
|
try:
|
|
# 'aki','ari','ami' container formats are supported by glance,
|
|
# but they need matching disk format to use.
|
|
# Glance usually uses 'bare' for other disk formats except
|
|
# amazon's. Please check the comment in CreateImageForm class
|
|
cinder.volume_upload_to_image(request,
|
|
volume_id,
|
|
data['force'],
|
|
data['image_name'],
|
|
DEFAULT_CONTAINER_FORMAT,
|
|
data['disk_format'])
|
|
message = _(
|
|
'Successfully sent the request to upload volume to image '
|
|
'for volume: "%s"') % data['name']
|
|
messages.info(request, message)
|
|
|
|
return True
|
|
except Exception:
|
|
redirect = reverse("horizon:project:volumes:index")
|
|
error_message = _(
|
|
'Unable to upload volume to image for volume: "%s"') \
|
|
% data['name']
|
|
exceptions.handle(request, error_message, redirect=redirect)
|
|
|
|
|
|
class ExtendForm(forms.SelfHandlingForm):
|
|
name = forms.CharField(
|
|
label=_("Volume Name"),
|
|
widget=forms.TextInput(attrs={'readonly': 'readonly'}),
|
|
required=False,
|
|
)
|
|
orig_size = forms.IntegerField(
|
|
label=_("Current Size (GiB)"),
|
|
widget=forms.TextInput(attrs={'readonly': 'readonly'}),
|
|
required=False,
|
|
)
|
|
new_size = forms.IntegerField(label=_("New Size (GiB)"))
|
|
|
|
def clean(self):
|
|
cleaned_data = super(ExtendForm, self).clean()
|
|
new_size = cleaned_data.get('new_size')
|
|
orig_size = self.initial['orig_size']
|
|
if new_size <= orig_size:
|
|
error_msg = _("New size must be greater than current size.")
|
|
self._errors['new_size'] = self.error_class([error_msg])
|
|
return cleaned_data
|
|
|
|
usages = quotas.tenant_limit_usages(self.request)
|
|
availableGB = usages['maxTotalVolumeGigabytes'] - \
|
|
usages['gigabytesUsed']
|
|
if availableGB < (new_size - orig_size):
|
|
message = _('Volume cannot be extended to %(req)iGiB as '
|
|
'you only have %(avail)iGiB of your quota '
|
|
'available.')
|
|
params = {'req': new_size, 'avail': availableGB}
|
|
self._errors["new_size"] = self.error_class([message % params])
|
|
return cleaned_data
|
|
|
|
def handle(self, request, data):
|
|
volume_id = self.initial['id']
|
|
try:
|
|
volume = cinder.volume_extend(request,
|
|
volume_id,
|
|
data['new_size'])
|
|
|
|
message = _('Extending volume: "%s"') % data['name']
|
|
messages.info(request, message)
|
|
return volume
|
|
except Exception:
|
|
redirect = reverse("horizon:project:volumes:index")
|
|
exceptions.handle(request,
|
|
_('Unable to extend volume.'),
|
|
redirect=redirect)
|
|
|
|
|
|
class RetypeForm(forms.SelfHandlingForm):
|
|
name = forms.CharField(label=_('Volume Name'),
|
|
widget=forms.TextInput(
|
|
attrs={'readonly': 'readonly'}))
|
|
volume_type = forms.ThemableChoiceField(label=_('Type'))
|
|
MIGRATION_POLICY_CHOICES = [('never', _('Never')),
|
|
('on-demand', _('On Demand'))]
|
|
migration_policy = forms.ChoiceField(label=_('Migration Policy'),
|
|
widget=forms.ThemableSelectWidget(),
|
|
choices=MIGRATION_POLICY_CHOICES,
|
|
initial='never',
|
|
required=False)
|
|
|
|
def __init__(self, request, *args, **kwargs):
|
|
super(RetypeForm, self).__init__(request, *args, **kwargs)
|
|
|
|
try:
|
|
volume_types = cinder.volume_type_list(request)
|
|
except Exception:
|
|
redirect_url = reverse("horizon:project:volumes:index")
|
|
error_message = _('Unable to retrieve the volume type list.')
|
|
exceptions.handle(request, error_message, redirect=redirect_url)
|
|
|
|
origin_type = self.initial['volume_type']
|
|
types_list = [(t.name, t.name)
|
|
for t in volume_types
|
|
if t.name != origin_type]
|
|
|
|
if types_list:
|
|
types_list.insert(0, ("", _("Select a new volume type")))
|
|
else:
|
|
types_list.insert(0, ("", _("No other volume types available")))
|
|
self.fields['volume_type'].choices = sorted(types_list)
|
|
|
|
def handle(self, request, data):
|
|
volume_id = self.initial['id']
|
|
|
|
try:
|
|
cinder.volume_retype(request,
|
|
volume_id,
|
|
data['volume_type'],
|
|
data['migration_policy'])
|
|
|
|
message = _(
|
|
'Successfully sent the request to change the volume '
|
|
'type to "%(vtype)s" for volume: "%(name)s"')
|
|
params = {'name': data['name'],
|
|
'vtype': data['volume_type']}
|
|
messages.info(request, message % params)
|
|
|
|
return True
|
|
except Exception:
|
|
redirect = reverse("horizon:project:volumes:index")
|
|
error_message = _(
|
|
'Unable to change the volume type for volume: "%s"') \
|
|
% data['name']
|
|
exceptions.handle(request, error_message, redirect=redirect)
|