e5d09edc20
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
872 lines
38 KiB
Python
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)
|