Files
manila-ui/manila_ui/dashboards/project/shares/forms.py
Goutham Pacha Ravi 37e5b2f053 Fix parsing names in switched fields
In the share creation form dialog, share network
selection is optionally provided based on whether
the share type chosen supports the DHSS extra-spec.
This selection breaks often when dealing with share
types that have a name matching the format: ".*-\d+.*".
For example: test-700. The root cause of this seems to
be in javascript code for "switchable" fields [1] that
doesn't get triggered as expected.

A similar issue manifests in the share Network Creation
form where we setup switched fields with the Neutron
network IDs (dashed format UUIDs) and in the Share
Group Creation form where we setup switched fields
with the Share Group Type IDs (dashed format UUIDs).

So, we could encode the "-" in these field names to
workaround this issue.

Closes-Bug: #1931641
[1] 647c2b7530/horizon/static/horizon/js/horizon.forms.js (L491-L613)

Change-Id: Id924fc55debdc38ae2131bf8cef396f28caa3e77
Signed-off-by: Goutham Pacha Ravi <gouthampravi@gmail.com>
2021-06-16 12:14:17 -07:00

476 lines
20 KiB
Python

# Copyright (c) 2014 NetApp, 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.
from django.conf import settings
from django.forms import ValidationError
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from horizon.utils.memoized import memoized
from manila_ui.api import manila
from manila_ui.dashboards import utils
from manila_ui import features
from manilaclient.common.apiclient import exceptions as m_exceptions
import time
class CreateForm(forms.SelfHandlingForm):
name = forms.CharField(max_length="255", label=_("Share Name"))
description = forms.CharField(
label=_("Description"), required=False,
widget=forms.Textarea(attrs={'rows': 3}))
share_proto = forms.ChoiceField(label=_("Share Protocol"), required=True)
size = forms.IntegerField(min_value=1, label=_("Size (GiB)"))
share_type = forms.ChoiceField(
label=_("Share Type"), required=True,
widget=forms.Select(
attrs={'class': 'switchable', 'data-slug': 'sharetype'}))
availability_zone = forms.ChoiceField(
label=_("Availability Zone"),
required=False)
def __init__(self, request, *args, **kwargs):
super(CreateForm, self).__init__(request, *args, **kwargs)
# NOTE(vkmc): choose only those share protocols that are enabled
# FIXME(vkmc): this should be better implemented by having a
# capabilities endpoint on the control plane.
manila_features = getattr(settings, 'OPENSTACK_MANILA_FEATURES', {})
self.enabled_share_protocols = manila_features.get(
'enabled_share_protocols',
['NFS', 'CIFS', 'GlusterFS', 'HDFS', 'CephFS', 'MapRFS'])
self.enable_public_shares = manila_features.get(
'enable_public_shares', True)
share_networks = manila.share_network_list(request)
share_types = manila.share_type_list(request)
self.fields['share_type'].choices = (
[("", "")] +
[(utils.transform_dashed_name(st.name), st.name)
for st in share_types]
)
availability_zones = manila.availability_zone_list(request)
self.fields['availability_zone'].choices = (
[("", "")] + [(az.name, az.name) for az in availability_zones])
if features.is_share_groups_enabled():
share_groups = manila.share_group_list(request)
self.fields['sg'] = forms.ChoiceField(
label=_("Share Group"),
choices=[("", "")] + [(sg.id, sg.name or sg.id)
for sg in share_groups],
required=False)
self.sn_field_name_prefix = 'share-network-choices-'
for st in share_types:
extra_specs = st.get_keys()
dhss = extra_specs.get('driver_handles_share_servers')
# NOTE(vponomaryov): Set and tie share-network field only for
# share types with enabled handling of share servers.
if (isinstance(dhss, str) and dhss.lower() in ['true', '1']):
sn_choices = (
[('', '')] +
[(sn.id, sn.name or sn.id) for sn in share_networks])
sn_field_name = (
self.sn_field_name_prefix +
utils.transform_dashed_name(st.name)
)
sn_field = forms.ChoiceField(
label=_("Share Network"), required=True,
choices=sn_choices,
widget=forms.Select(attrs={
'class': 'switched',
'data-switch-on': 'sharetype',
'data-sharetype-%s' % utils.transform_dashed_name(
st.name): _("Share Network"),
}))
self.fields[sn_field_name] = sn_field
self.fields['share_source_type'] = forms.ChoiceField(
label=_("Share Source"), required=False,
widget=forms.Select(
attrs={'class': 'switchable', 'data-slug': 'source'}))
self.fields['snapshot'] = forms.ChoiceField(
label=_("Use snapshot as a source"),
widget=forms.fields.SelectWidget(
attrs={'class': 'switched',
'data-switch-on': 'source',
'data-source-snapshot': _('Snapshot')},
data_attrs=('size', 'name'),
transform=lambda x: "%s (%sGiB)" % (x.name, x.size)),
required=False)
self.fields['metadata'] = forms.CharField(
label=_("Metadata"), required=False,
widget=forms.Textarea(attrs={'rows': 4}))
if self.enable_public_shares:
self.fields['is_public'] = forms.BooleanField(
label=_("Make visible to users from all projects"),
required=False)
self.fields['share_proto'].choices = [(sp, sp) for sp in
self.enabled_share_protocols]
if ("snapshot_id" in request.GET or
kwargs.get("data", {}).get("snapshot")):
try:
snapshot = self.get_snapshot(
request,
request.GET.get("snapshot_id",
kwargs.get("data", {}).get("snapshot")))
self.fields['name'].initial = snapshot.name
self.fields['size'].initial = snapshot.size
self.fields['snapshot'].choices = ((snapshot.id, snapshot),)
try:
# Set the share type from the original share
orig_share = manila.share_get(request, snapshot.share_id)
# NOTE(vponomaryov): we should use share type name, not ID,
# because we use names in our choices above.
self.fields['share_type'].initial = (
orig_share.share_type_name)
except Exception:
pass
self.fields['size'].help_text = _(
'Share size must be equal to or greater than the snapshot '
'size (%sGiB)') % snapshot.size
del self.fields['share_source_type']
except Exception:
exceptions.handle(request,
_('Unable to load the specified snapshot.'))
else:
source_type_choices = []
try:
snapshot_list = manila.share_snapshot_list(request)
snapshots = [s for s in snapshot_list
if s.status == 'available']
if snapshots:
source_type_choices.append(("snapshot",
_("Snapshot")))
choices = [('', _("Choose a snapshot"))] + \
[(s.id, s) for s in snapshots]
self.fields['snapshot'].choices = choices
else:
del self.fields['snapshot']
except Exception:
exceptions.handle(request, _("Unable to retrieve "
"share snapshots."))
if source_type_choices:
choices = ([('no_source_type',
_("No source, empty share"))] +
source_type_choices)
self.fields['share_source_type'].choices = choices
else:
del self.fields['share_source_type']
def clean(self):
cleaned_data = super(CreateForm, self).clean()
form_errors = list(self.errors)
chosen_share_type = cleaned_data.get('share_type')
if chosen_share_type:
# NOTE(vponomaryov): skip errors for share-network fields that are
# not related to chosen share type.
for error in form_errors:
st_name = error.split(self.sn_field_name_prefix)[-1]
if (error.startswith(self.sn_field_name_prefix) and
st_name != chosen_share_type):
cleaned_data[error] = 'Not set'
self.errors.pop(error, None)
cleaned_data['share_network'] = cleaned_data.get(
self.sn_field_name_prefix + cleaned_data.get('share_type'))
return cleaned_data
def handle(self, request, data):
try:
snapshot_id = None
source_type = data.get('share_source_type', None)
share_network = data.get('share_network', None)
if (data.get("snapshot", None) and
source_type in [None, 'snapshot']):
# Create from Snapshot
snapshot = self.get_snapshot(request,
data["snapshot"])
snapshot_id = snapshot.id
if (data['size'] < snapshot.size):
error_message = _('The share size cannot be less than the '
'snapshot size (%sGiB)') % snapshot.size
raise ValidationError(error_message)
else:
data['size'] = int(data['size'])
metadata = {}
try:
set_dict, unset_list = utils.parse_str_meta(data['metadata'])
if unset_list:
msg = _("Expected only pairs of key=value.")
raise ValidationError(message=msg)
metadata = set_dict
except ValidationError as e:
self.api_error(e.messages[0])
return False
is_public = self.enable_public_shares and data['is_public']
share = manila.share_create(
request,
size=data['size'],
name=data['name'],
description=data['description'],
proto=data['share_proto'],
share_network=share_network,
snapshot_id=snapshot_id,
share_type=utils.transform_dashed_name(data['share_type']),
is_public=is_public,
metadata=metadata,
availability_zone=data['availability_zone'],
share_group_id=data.get('sg') or None,
)
message = _('Creating share "%s"') % data['name']
messages.success(request, message)
return share
except ValidationError as e:
self.api_error(e.messages[0])
except m_exceptions.BadRequest as e:
self.api_error(_("Unable to create share. %s") % e.message)
except Exception:
exceptions.handle(request, ignore=True)
self.api_error(_("Unable to create share."))
return False
@memoized
def get_snapshot(self, request, id):
return manila.share_snapshot_get(request, id)
class UpdateForm(forms.SelfHandlingForm):
name = forms.CharField(max_length="255", label=_("Share Name"))
description = forms.CharField(widget=forms.Textarea,
label=_("Description"), required=False)
is_public = forms.BooleanField(
label=_("Make visible to users from all projects"),
required=False)
def __init__(self, request, *args, **kwargs):
super(UpdateForm, self).__init__(request, *args, **kwargs)
manila_features = getattr(settings, 'OPENSTACK_MANILA_FEATURES', {})
self.enable_public_shares = manila_features.get(
'enable_public_shares', True)
if not self.enable_public_shares:
self.fields.pop('is_public')
def handle(self, request, data):
share_id = self.initial['share_id']
is_public = data['is_public'] if self.enable_public_shares else False
try:
share = manila.share_get(self.request, share_id)
manila.share_update(
request, share, data['name'], data['description'],
is_public=is_public)
message = _('Updating share "%s"') % data['name']
messages.success(request, message)
return True
except Exception:
redirect = reverse("horizon:project:shares:index")
exceptions.handle(request,
_('Unable to update share.'),
redirect=redirect)
class UpdateMetadataForm(forms.SelfHandlingForm):
metadata = forms.CharField(widget=forms.Textarea,
label=_("Metadata"), required=False)
def __init__(self, *args, **kwargs):
super(UpdateMetadataForm, self).__init__(*args, **kwargs)
meta_str = ""
for k, v in self.initial["metadata"].items():
meta_str += "%s=%s\r\n" % (k, v)
self.initial["metadata"] = meta_str
def handle(self, request, data):
share_id = self.initial['share_id']
try:
share = manila.share_get(self.request, share_id)
set_dict, unset_list = utils.parse_str_meta(data['metadata'])
if set_dict:
manila.share_set_metadata(request, share, set_dict)
if unset_list:
manila.share_delete_metadata(request, share, unset_list)
message = _('Updating share metadata "%s"') % share.name
messages.success(request, message)
return True
except ValidationError as e:
self.api_error(e.messages[0])
return False
except Exception:
redirect = reverse("horizon:project:shares:index")
exceptions.handle(request,
_('Unable to update share metadata.'),
redirect=redirect)
class AddRule(forms.SelfHandlingForm):
access_type = forms.ChoiceField(
label=_("Access Type"), required=True,
choices=(('ip', 'ip'), ('user', 'user'), ('cert', 'cert'),
('cephx', 'cephx')))
access_level = forms.ChoiceField(
label=_("Access Level"), required=True,
choices=(('rw', 'read-write'), ('ro', 'read-only'),))
access_to = forms.CharField(
label=_("Access To"), max_length="255", required=True)
def handle(self, request, data):
share_id = self.initial['share_id']
try:
manila.share_allow(
request, share_id,
access_to=data['access_to'],
access_type=data['access_type'],
access_level=data['access_level'])
message = _('Creating rule for "%s"') % data['access_to']
messages.success(request, message)
return True
except Exception:
redirect = reverse("horizon:project:shares:manage_rules",
args=[self.initial['share_id']])
exceptions.handle(
request, _('Unable to add rule.'), redirect=redirect)
class ResizeForm(forms.SelfHandlingForm):
name = forms.CharField(
max_length="255", label=_("Share 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(ResizeForm, self).clean()
new_size = cleaned_data.get('new_size')
orig_size = self.initial['orig_size']
if new_size == orig_size:
message = _("New size must be different than the existing size")
self._errors["new_size"] = self.error_class([message])
return cleaned_data
if new_size <= 0:
message = _("New size should not be less than or equal to zero")
self._errors["new_size"] = self.error_class([message])
return cleaned_data
usages = manila.tenant_absolute_limits(self.request)
availableGB = (usages['maxTotalShareGigabytes'] -
usages['totalShareGigabytesUsed'])
if availableGB < (new_size - orig_size):
message = _('Share cannot be extended to %(req)iGiB as '
'you only have %(avail)iGiB of your quota '
'available.')
params = {'req': new_size, 'avail': availableGB + orig_size}
self._errors["new_size"] = self.error_class([message % params])
return cleaned_data
def handle(self, request, data):
share_id = self.initial['share_id']
new_size = data['new_size']
orig_size = data['orig_size']
try:
manila.share_resize(request, share_id, new_size, orig_size)
return self.check_size(request, share_id, new_size)
except Exception:
redirect = reverse("horizon:project:shares:index")
exceptions.handle(request, _(
'Unable to resize share.'), redirect=redirect)
def check_size(self, request, share_id, new_size):
share = manila.share_get(request, share_id)
timeout = 30
interval = 0.35
time_elapsed = 0
while share.status != 'available':
time.sleep(interval)
time_elapsed += interval
share = manila.share_get(request, share_id)
if time_elapsed > timeout:
raise exceptions.WorkflowError(_(
"The operation timed out while resizing. "
"Please try again."))
if share.size == new_size:
message = _('Resized share "%s"') % share.name
messages.success(request, message)
return True
raise exceptions.WorkflowError(_(
"Unable to resize share. "))
class RevertForm(forms.SelfHandlingForm):
"""Form for reverting a share to a snapshot."""
snapshot = forms.ChoiceField(
label=_("Snapshot"),
required=True,
widget=forms.Select(
attrs={'class': 'switchable', 'data-slug': 'share_snapshot'}))
def __init__(self, req, *args, **kwargs):
super(self.__class__, self).__init__(req, *args, **kwargs)
# NOTE(vponomaryov): manila client does not allow to filter snapshots
# using "created_at" field, so, we need to get all snapshots of a share
# and do filtering here.
search_opts = {'share_id': self.initial['share_id']}
snapshots = manila.share_snapshot_list(req, search_opts=search_opts)
amount_of_snapshots = len(snapshots)
if amount_of_snapshots < 1:
self.fields['snapshot'].choices = [("", "")]
else:
snapshot = snapshots[0]
if amount_of_snapshots > 1:
for s in snapshots[1:]:
if s.created_at > snapshot.created_at:
snapshot = s
self.fields['snapshot'].choices = [
(snapshot.id, snapshot.name or snapshot.id)]
def handle(self, request, data):
share_id = self.initial['share_id']
snapshot_id = data['snapshot']
try:
manila.share_revert(request, share_id, snapshot_id)
message = _('Share "%(s)s" has been reverted to "%(ss)s" snapshot '
'successfully') % {'s': share_id, 'ss': snapshot_id}
messages.success(request, message)
return True
except Exception:
redirect = reverse("horizon:project:shares:index")
exceptions.handle(
request,
_('Unable to revert share to the snapshot.'),
redirect=redirect)