When creating a volume and selecting to use image as a source we get a dropdown of the images that we can select but this list is not sorted based on image name. This changes so that it's sorted by the image name ascending to match the instance launch dialog that already lists images in ascending order by name. Change-Id: I17212e460e307e08b2b6c2a8a68a14ffde8cc04b
874 lines
38 KiB
Python
874 lines
38 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 cinderclient import exceptions as cinder_exc
|
|
|
|
from django.conf import settings
|
|
from django.forms import ValidationError
|
|
from django.template.defaultfilters import filesizeformat
|
|
from django.urls import reverse
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.utils.translation import pgettext_lazy
|
|
|
|
from horizon import exceptions
|
|
from horizon import forms
|
|
from horizon import messages
|
|
from horizon.utils import functions
|
|
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.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 = settings.OPENSTACK_IMAGE_BACKEND
|
|
IMAGE_FORMAT_CHOICES = IMAGE_BACKEND_SETTINGS['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 switched',
|
|
'data-switch-on': 'source',
|
|
'data-source-snapshot_source':
|
|
_("Use snapshot as a source"),
|
|
'data-required-when-shown': 'true'},
|
|
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 switched',
|
|
'data-switch-on': 'source',
|
|
'data-source-image_source':
|
|
_("Use image as a source"),
|
|
'data-required-when-shown': 'true'},
|
|
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 switched',
|
|
'data-switch-on': 'source',
|
|
'data-source-volume_source':
|
|
_("Use a volume as source"),
|
|
'data-required-when-shown': 'true'},
|
|
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')}))
|
|
group = forms.ThemableChoiceField(
|
|
label=_("Group"), required=False,
|
|
help_text=_("Group which the new volume belongs to. Choose "
|
|
"'No group' if the new volume belongs to no group."))
|
|
|
|
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 = []
|
|
for image in images:
|
|
image.bytes = image.size
|
|
image.size = functions.bytes_to_gigabytes(image.bytes)
|
|
choices.append((image.id, image))
|
|
sorted_choices = sorted(
|
|
choices, key=lambda x: (x[1].name if x[1].name else ''))
|
|
sorted_choices.insert(0, ('', _("Choose an image")))
|
|
self.fields['image_source'].choices = sorted_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 _populate_group_choices(self, request):
|
|
try:
|
|
groups = cinder.group_list(request)
|
|
except cinder_exc.VersionNotFoundForAPIMethod:
|
|
del self.fields['group']
|
|
return
|
|
except Exception:
|
|
redirect = reverse("horizon:project:volumes:index")
|
|
exceptions.handle(request,
|
|
_('Unable to retrieve the volume group list.'),
|
|
redirect=redirect)
|
|
group_choices = [(g.id, g.name or g.id) for g in groups]
|
|
group_choices.insert(0, ("", _("No group")))
|
|
self.fields['group'].choices = group_choices
|
|
|
|
def __init__(self, request, *args, **kwargs):
|
|
super().__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)
|
|
|
|
self._populate_group_choices(request)
|
|
|
|
def clean(self):
|
|
cleaned_data = super().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_quota_usages(
|
|
self.request, targets=('volumes', 'gigabytes'))
|
|
availableGB = usages['gigabytes']['available']
|
|
availableVol = usages['volumes']['available']
|
|
|
|
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
|
|
volume_type = None
|
|
|
|
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)
|
|
if 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,
|
|
group_id=data.get('group') or None)
|
|
message = _('Creating volume "%s"') % volume.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().__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"),
|
|
required=False)
|
|
description = forms.CharField(max_length=255,
|
|
widget=forms.Textarea(attrs={'rows': 4}),
|
|
label=_("Description"),
|
|
required=False)
|
|
|
|
def __init__(self, request, *args, **kwargs):
|
|
super().__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".')
|
|
if volume.status == 'in-use':
|
|
force = True
|
|
message = _('Forcing to create snapshot "%s" '
|
|
'from attached volume.')
|
|
snapshot = cinder.volume_snapshot_create(request,
|
|
data['volume_id'],
|
|
data['name'],
|
|
data['description'],
|
|
force=force)
|
|
|
|
messages.info(request, message % snapshot.name)
|
|
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 __init__(self, *args, **kwargs):
|
|
self.next_view = kwargs.pop('next_view', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
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):
|
|
volume_id = self.initial['volume_id']
|
|
try:
|
|
transfer = cinder.transfer_create(request, volume_id, data['name'])
|
|
except Exception:
|
|
redirect = reverse("horizon:project:volumes:index")
|
|
exceptions.handle(request, _('Unable to create volume transfer.'),
|
|
redirect=redirect)
|
|
else:
|
|
msg = _('Created volume transfer: "%s".') % data['name']
|
|
messages.success(request, msg)
|
|
request.method = 'GET'
|
|
return self.next_view.as_view()(
|
|
request, transfer_id=transfer.id,
|
|
auth_key=transfer.auth_key,
|
|
)
|
|
|
|
|
|
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):
|
|
return True
|
|
|
|
|
|
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:
|
|
volume = 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)
|
|
|
|
name_or_id = volume["volume"]["name"] or volume["volume"]["id"]
|
|
message = _('Updating volume "%s"') % name_or_id
|
|
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",
|
|
"Force"),
|
|
widget=forms.CheckboxInput(),
|
|
required=False)
|
|
|
|
def __init__(self, request, *args, **kwargs):
|
|
super().__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 glance.get_image_formats(request)
|
|
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().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_quota_usages(
|
|
self.request, targets=('gigabytes',))
|
|
availableGB = usages['gigabytes']['available']
|
|
|
|
if availableGB < (new_size - orig_size):
|
|
message = _('Volume cannot be extended to %(req)iGiB as '
|
|
'the maximum size it can be extended to is '
|
|
'%(max_size)iGiB.')
|
|
params = {'req': new_size, 'max_size': (availableGB + orig_size)}
|
|
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().__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']
|
|
type_list = [(t.name,
|
|
_("%s (current)") % t.name
|
|
if origin_type == t.name else t.name)
|
|
for t in volume_types]
|
|
|
|
if not type_list:
|
|
type_list.insert(0, ("", _("No other volume types available")))
|
|
self.fields['volume_type'].choices = sorted(type_list)
|
|
|
|
def clean_volume_type(self):
|
|
volume_type = self.cleaned_data.get("volume_type")
|
|
if self.initial['volume_type'] == volume_type:
|
|
msg = _('The new type must be different from the '
|
|
'current volume type.')
|
|
raise forms.ValidationError(msg)
|
|
return volume_type
|
|
|
|
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)
|