horizon/openstack_dashboard/dashboards/project/volumes/forms.py
Akihiro Motoki e5d09edc20 Use python3-style super()
In python3, super() does not always require a class and self reference.
In other words, super() is enough for most cases.
This is much simpler and it is time to switch it to the newer style.

pylint provides a check for this.
Let's enable 'super-with-arguments' check.

NOTE: _prepare_mappings() method of FormRegion in
openstack_dashboard/test/integration_tests/regions/forms.py is refactored.
super() (without explicit class and self referece) does not work when
a subclass method calls a same method in a parent class multiple times.
It looks better to prepare a separate method to provide a common logic.

Change-Id: Id9512a14be9f20dbd5ebd63d446570c7b7c825ff
2020-10-15 14:37:20 +09:00

872 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 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
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 = [('', _("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 _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):
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)
kwargs = {
'transfer_id': transfer.id,
'auth_key': transfer.auth_key
}
request.method = 'GET'
return self.next_view.as_view()(request, **kwargs)
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:
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",
u"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)