
The generic group type named "default_cgsnapshot_type" is reserved for the consistency group and we cannot use a generic group using "default_cgsnapshot_type". "default_cgsnapshot_type" should not be listed in "Group Type" selection in "Create Group" form. In addition, "Group" and "Group Snapshot" panels make no sense when there is no group type other than "default_cgsnapshot_type". This commit adds a suggestion to hide "Group" and "Group Snapshot" panels if there is no valid group type to the release notes. Closes-Bug: #1785178 Change-Id: I2dce39bcbcf6bedc8dc0c94d11f3c0a85ea61490
382 lines
15 KiB
Python
382 lines
15 KiB
Python
# 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.utils.translation import ugettext_lazy as _
|
|
|
|
from horizon import exceptions
|
|
from horizon import forms
|
|
from horizon import workflows
|
|
|
|
from openstack_dashboard import api
|
|
from openstack_dashboard.api import cinder
|
|
|
|
INDEX_URL = "horizon:project:volume_groups:index"
|
|
CGROUP_VOLUME_MEMBER_SLUG = "update_members"
|
|
|
|
|
|
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 AddGroupInfoAction(workflows.Action):
|
|
name = forms.CharField(label=_("Name"),
|
|
max_length=255)
|
|
description = forms.CharField(widget=forms.widgets.Textarea(
|
|
attrs={'rows': 4}),
|
|
label=_("Description"),
|
|
required=False)
|
|
group_type = forms.ChoiceField(
|
|
label=_("Group Type"),
|
|
widget=forms.ThemableSelectWidget())
|
|
availability_zone = forms.ChoiceField(
|
|
label=_("Availability Zone"),
|
|
required=False,
|
|
widget=forms.ThemableSelectWidget(
|
|
attrs={'class': 'switched',
|
|
'data-switch-on': 'source',
|
|
'data-source-no_source_type': _('Availability Zone'),
|
|
'data-source-image_source': _('Availability Zone')}))
|
|
|
|
def __init__(self, request, *args, **kwargs):
|
|
super(AddGroupInfoAction, self).__init__(request,
|
|
*args,
|
|
**kwargs)
|
|
self.fields['availability_zone'].choices = \
|
|
availability_zones(request)
|
|
try:
|
|
# Group type name 'default_cgsnapshot_type' is reserved for
|
|
# consistency group and it cannot be used for a group type.
|
|
# Let's exclude it.
|
|
group_types = [(t.id, t.name) for t
|
|
in api.cinder.group_type_list(request)
|
|
if t.name != 'default_cgsnapshot_type']
|
|
except Exception:
|
|
exceptions.handle(request, _('Unable to retrieve group types.'))
|
|
if group_types:
|
|
group_types.insert(0, ("", _("Select group type")))
|
|
else:
|
|
group_types.insert(0, ("", _("No valid group type")))
|
|
self.fields['group_type'].choices = group_types
|
|
|
|
class Meta(object):
|
|
name = _("Group Information")
|
|
help_text = _("Volume groups provide a mechanism for "
|
|
"creating snapshots of multiple volumes at the same "
|
|
"point-in-time to ensure data consistency\n\n"
|
|
"A volume group can support more than one volume "
|
|
"type, but it can only contain volumes hosted by the "
|
|
"same back end.")
|
|
slug = "set_group_info"
|
|
|
|
def clean(self):
|
|
cleaned_data = super(AddGroupInfoAction, self).clean()
|
|
name = cleaned_data.get('name')
|
|
|
|
try:
|
|
groups = cinder.group_list(self.request)
|
|
except Exception:
|
|
msg = _('Unable to get group list')
|
|
exceptions.check_message(["Connection", "refused"], msg)
|
|
raise
|
|
|
|
if groups is not None and name is not None:
|
|
for group in groups:
|
|
if group.name.lower() == name.lower():
|
|
# ensure new name has reasonable length
|
|
formatted_name = name
|
|
if len(name) > 20:
|
|
formatted_name = name[:14] + "..." + name[-3:]
|
|
raise forms.ValidationError(
|
|
_('The name "%s" is already used by '
|
|
'another group.')
|
|
% formatted_name
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class AddGroupInfoStep(workflows.Step):
|
|
action_class = AddGroupInfoAction
|
|
contributes = ("availability_zone", "group_type",
|
|
"description",
|
|
"name")
|
|
|
|
|
|
class AddVolumeTypesToGroupAction(workflows.MembershipAction):
|
|
def __init__(self, request, *args, **kwargs):
|
|
super(AddVolumeTypesToGroupAction, self).__init__(request,
|
|
*args,
|
|
**kwargs)
|
|
err_msg = _('Unable to get the available volume types')
|
|
|
|
default_role_field_name = self.get_default_role_field_name()
|
|
self.fields[default_role_field_name] = forms.CharField(required=False)
|
|
self.fields[default_role_field_name].initial = 'member'
|
|
|
|
field_name = self.get_member_field_name('member')
|
|
self.fields[field_name] = forms.MultipleChoiceField(required=False)
|
|
|
|
vtypes = []
|
|
try:
|
|
vtypes = cinder.volume_type_list(request)
|
|
except Exception:
|
|
exceptions.handle(request, err_msg)
|
|
|
|
vtype_list = [(vtype.id, vtype.name)
|
|
for vtype in vtypes]
|
|
self.fields[field_name].choices = vtype_list
|
|
|
|
class Meta(object):
|
|
name = _("Manage Volume Types")
|
|
slug = "add_vtypes_to_group"
|
|
|
|
def clean(self):
|
|
cleaned_data = super(AddVolumeTypesToGroupAction, self).clean()
|
|
volume_types = cleaned_data.get('add_vtypes_to_group_role_member')
|
|
if not volume_types:
|
|
raise forms.ValidationError(
|
|
_('At least one volume type must be assigned '
|
|
'to a group.')
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class AddVolTypesToGroupStep(workflows.UpdateMembersStep):
|
|
action_class = AddVolumeTypesToGroupAction
|
|
help_text = _("Add volume types to this group. "
|
|
"Multiple volume types can be added to the same "
|
|
"group only if they are associated with "
|
|
"same back end.")
|
|
available_list_title = _("All available volume types")
|
|
members_list_title = _("Selected volume types")
|
|
no_available_text = _("No volume types found.")
|
|
no_members_text = _("No volume types selected.")
|
|
show_roles = False
|
|
contributes = ("volume_types",)
|
|
|
|
def contribute(self, data, context):
|
|
if data:
|
|
member_field_name = self.get_member_field_name('member')
|
|
context['volume_types'] = data.get(member_field_name, [])
|
|
return context
|
|
|
|
|
|
class AddVolumesToGroupAction(workflows.MembershipAction):
|
|
def __init__(self, request, *args, **kwargs):
|
|
super(AddVolumesToGroupAction, self).__init__(request,
|
|
*args,
|
|
**kwargs)
|
|
err_msg = _('Unable to get the available volumes')
|
|
|
|
default_role_field_name = self.get_default_role_field_name()
|
|
self.fields[default_role_field_name] = forms.CharField(required=False)
|
|
self.fields[default_role_field_name].initial = 'member'
|
|
|
|
field_name = self.get_member_field_name('member')
|
|
self.fields[field_name] = forms.MultipleChoiceField(required=False)
|
|
|
|
vtypes = self.initial['vtypes']
|
|
try:
|
|
# get names of volume types associated with group
|
|
vtype_names = []
|
|
volume_types = cinder.volume_type_list(request)
|
|
for volume_type in volume_types:
|
|
if volume_type.id in vtypes:
|
|
vtype_names.append(volume_type.name)
|
|
|
|
# collect volumes that are associated with volume types
|
|
vol_list = []
|
|
volumes = cinder.volume_list(request)
|
|
for volume in volumes:
|
|
if volume.volume_type in vtype_names:
|
|
group_id = None
|
|
vol_is_available = False
|
|
in_this_group = False
|
|
if hasattr(volume, 'group_id'):
|
|
# this vol already belongs to a group
|
|
# only include it here if it belongs to this group
|
|
group_id = volume.group_id
|
|
|
|
if not group_id:
|
|
# put this vol in the available list
|
|
vol_is_available = True
|
|
elif group_id == self.initial['group_id']:
|
|
# put this vol in the assigned to group list
|
|
vol_is_available = True
|
|
in_this_group = True
|
|
|
|
if vol_is_available:
|
|
vol_list.append({'volume_name': volume.name,
|
|
'volume_id': volume.id,
|
|
'in_group': in_this_group,
|
|
'is_duplicate': False})
|
|
|
|
sorted_vol_list = sorted(vol_list, key=lambda k: k['volume_name'])
|
|
|
|
# mark any duplicate volume names
|
|
for index, volume in enumerate(sorted_vol_list):
|
|
if index < len(sorted_vol_list) - 1:
|
|
if volume['volume_name'] == \
|
|
sorted_vol_list[index + 1]['volume_name']:
|
|
volume['is_duplicate'] = True
|
|
sorted_vol_list[index + 1]['is_duplicate'] = True
|
|
|
|
# update display with all available vols and those already
|
|
# assigned to group
|
|
available_vols = []
|
|
assigned_vols = []
|
|
for volume in sorted_vol_list:
|
|
if volume['is_duplicate']:
|
|
# add id to differentiate volumes to user
|
|
entry = volume['volume_name'] + \
|
|
" [" + volume['volume_id'] + "]"
|
|
else:
|
|
entry = volume['volume_name']
|
|
available_vols.append((volume['volume_id'], entry))
|
|
if volume['in_group']:
|
|
assigned_vols.append(volume['volume_id'])
|
|
|
|
except Exception:
|
|
exceptions.handle(request, err_msg)
|
|
|
|
self.fields[field_name].choices = available_vols
|
|
self.fields[field_name].initial = assigned_vols
|
|
|
|
class Meta(object):
|
|
name = _("Manage Volumes")
|
|
slug = "add_volumes_to_group"
|
|
|
|
|
|
class AddVolumesToGroupStep(workflows.UpdateMembersStep):
|
|
action_class = AddVolumesToGroupAction
|
|
help_text = _("Add/remove volumes to/from this group. "
|
|
"Only volumes associated with the volume type(s) assigned "
|
|
"to this group will be available for selection.")
|
|
available_list_title = _("All available volumes")
|
|
members_list_title = _("Selected volumes")
|
|
no_available_text = _("No volumes found.")
|
|
no_members_text = _("No volumes selected.")
|
|
show_roles = False
|
|
depends_on = ("group_id", "name", "vtypes")
|
|
contributes = ("volumes",)
|
|
|
|
def contribute(self, data, context):
|
|
if data:
|
|
member_field_name = self.get_member_field_name('member')
|
|
context['volumes'] = data.get(member_field_name, [])
|
|
return context
|
|
|
|
|
|
class CreateGroupWorkflow(workflows.Workflow):
|
|
slug = "create_group"
|
|
name = _("Create Group")
|
|
finalize_button_name = _("Create Group")
|
|
failure_message = _('Unable to create group.')
|
|
success_message = _('Created new volume group')
|
|
success_url = INDEX_URL
|
|
default_steps = (AddGroupInfoStep,
|
|
AddVolTypesToGroupStep)
|
|
|
|
def handle(self, request, context):
|
|
try:
|
|
self.object = cinder.group_create(
|
|
request,
|
|
context['name'],
|
|
context['group_type'],
|
|
context['volume_types'],
|
|
description=context['description'],
|
|
availability_zone=context['availability_zone'])
|
|
except Exception:
|
|
exceptions.handle(request, _('Unable to create group.'))
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
class UpdateGroupWorkflow(workflows.Workflow):
|
|
slug = "update_group"
|
|
name = _("Add/Remove Group Volumes")
|
|
finalize_button_name = _("Submit")
|
|
success_message = _('Updated volumes for group.')
|
|
failure_message = _('Unable to update volumes for group')
|
|
success_url = INDEX_URL
|
|
default_steps = (AddVolumesToGroupStep,)
|
|
|
|
def handle(self, request, context):
|
|
group_id = context['group_id']
|
|
add_vols = []
|
|
remove_vols = []
|
|
try:
|
|
selected_volumes = context['volumes']
|
|
volumes = cinder.volume_list(request)
|
|
|
|
# scan all volumes and make correct consistency group is set
|
|
for volume in volumes:
|
|
selected = False
|
|
for selection in selected_volumes:
|
|
if selection == volume.id:
|
|
selected = True
|
|
break
|
|
|
|
if selected:
|
|
# ensure this volume is in this consistency group
|
|
if hasattr(volume, 'group_id'):
|
|
if volume.group_id != group_id:
|
|
add_vols.append(volume.id)
|
|
else:
|
|
add_vols.append(volume.id)
|
|
else:
|
|
# ensure this volume is not in our consistency group
|
|
if hasattr(volume, 'group_id'):
|
|
if volume.group_id == group_id:
|
|
# remove from this group
|
|
remove_vols.append(volume.id)
|
|
|
|
if not add_vols and not remove_vols:
|
|
# nothing to change
|
|
return True
|
|
|
|
cinder.group_update(request, group_id,
|
|
add_volumes=add_vols,
|
|
remove_volumes=remove_vols)
|
|
|
|
except Exception:
|
|
# error message supplied by form
|
|
return False
|
|
|
|
return True
|