# 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:volumes:index"
CGROUP_VOLUME_MEMBER_SLUG = "update_members"
[docs]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
[docs]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
[docs]class AddCGroupInfoAction(workflows.Action):
name = forms.CharField(label=_("Name"),
max_length=255)
description = forms.CharField(widget=forms.widgets.Textarea(
attrs={'rows': 4}),
label=_("Description"),
required=False)
availability_zone = forms.ChoiceField(
label=_("Availability Zone"),
required=False,
widget=forms.Select(
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(AddCGroupInfoAction, self).__init__(request,
*args,
**kwargs)
self.fields['availability_zone'].choices = \
availability_zones(request)
class Meta(object):
name = _("Consistency Group Information")
help_text = _("Volume consistency groups provide a mechanism for "
"creating snapshots of multiple volumes at the same "
"point-in-time to ensure data consistency\n\n"
"A consistency group can support more than one volume "
"type, but it can only contain volumes hosted by the "
"same back end.")
slug = "set_cgroup_info"
[docs] def clean(self):
cleaned_data = super(AddCGroupInfoAction, self).clean()
name = cleaned_data.get('name')
try:
cgroups = cinder.volume_cgroup_list(self.request)
except Exception:
msg = _('Unable to get consistency group list')
exceptions.check_message(["Connection", "refused"], msg)
raise
if cgroups is not None and name is not None:
for cgroup in cgroups:
if cgroup.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 consistency group.')
% formatted_name
)
return cleaned_data
[docs]class AddCGroupInfoStep(workflows.Step):
action_class = AddCGroupInfoAction
contributes = ("availability_zone",
"description",
"name")
[docs]class AddVolumeTypesToCGroupAction(workflows.MembershipAction):
def __init__(self, request, *args, **kwargs):
super(AddVolumeTypesToCGroupAction, 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_cgroup"
[docs] def clean(self):
cleaned_data = super(AddVolumeTypesToCGroupAction, self).clean()
volume_types = cleaned_data.get('add_vtypes_to_cgroup_role_member')
if not volume_types:
raise forms.ValidationError(
_('At least one volume type must be assigned '
'to a consistency group.')
)
return cleaned_data
[docs]class AddVolTypesToCGroupStep(workflows.UpdateMembersStep):
action_class = AddVolumeTypesToCGroupAction
help_text = _("Add volume types to this consistency group. "
"Multiple volume types can be added to the same "
"consistency 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",)
[docs] 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
[docs]class AddVolumesToCGroupAction(workflows.MembershipAction):
def __init__(self, request, *args, **kwargs):
super(AddVolumesToCGroupAction, 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 CG
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:
cgroup_id = None
vol_is_available = False
in_this_cgroup = False
if hasattr(volume, 'consistencygroup_id'):
# this vol already belongs to a CG.
# only include it here if it belongs to this CG
cgroup_id = volume.consistencygroup_id
if not cgroup_id:
# put this vol in the available list
vol_is_available = True
elif cgroup_id == self.initial['cgroup_id']:
# put this vol in the assigned to CG list
vol_is_available = True
in_this_cgroup = True
if vol_is_available:
vol_list.append({'volume_name': volume.name,
'volume_id': volume.id,
'in_cgroup': in_this_cgroup,
'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 consistency 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_cgroup']:
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_cgroup"
[docs]class AddVolumesToCGroupStep(workflows.UpdateMembersStep):
action_class = AddVolumesToCGroupAction
help_text = _("Add/remove volumes to/from this consistency group. "
"Only volumes associated with the volume type(s) assigned "
"to this consistency 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 = ("cgroup_id", "name", "vtypes")
contributes = ("volumes",)
[docs] 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
[docs]class CreateCGroupWorkflow(workflows.Workflow):
slug = "create_cgroup"
name = _("Create Consistency Group")
finalize_button_name = _("Create Consistency Group")
failure_message = _('Unable to create consistency group.')
success_message = _('Created new volume consistency group')
success_url = INDEX_URL
default_steps = (AddCGroupInfoStep,
AddVolTypesToCGroupStep)
[docs] def handle(self, request, context):
selected_vol_types = context['volume_types']
try:
vol_types = cinder.volume_type_list_with_qos_associations(
request)
except Exception:
msg = _('Unable to get volume type list')
exceptions.check_message(["Connection", "refused"], msg)
return False
# ensure that all selected volume types share same backend name
backend_name = None
invalid_backend = False
for selected_vol_type in selected_vol_types:
if not invalid_backend:
for vol_type in vol_types:
if selected_vol_type == vol_type.id:
if hasattr(vol_type, "extra_specs"):
vol_type_backend = \
vol_type.extra_specs['volume_backend_name']
if vol_type_backend is None:
invalid_backend = True
break
if backend_name is None:
backend_name = vol_type_backend
if vol_type_backend != backend_name:
invalid_backend = True
break
else:
invalid_backend = True
break
if invalid_backend:
msg = _('All selected volume types must be associated '
'with the same volume backend name.')
exceptions.handle(request, msg)
return False
try:
vtypes_str = ",".join(context['volume_types'])
self.object = \
cinder.volume_cgroup_create(
request,
vtypes_str,
context['name'],
description=context['description'],
availability_zone=context['availability_zone'])
except Exception:
exceptions.handle(request, _('Unable to create consistency '
'group.'))
return False
return True
[docs]class UpdateCGroupWorkflow(workflows.Workflow):
slug = "update_cgroup"
name = _("Add/Remove Consistency Group Volumes")
finalize_button_name = _("Submit")
success_message = _('Updated volumes for consistency group "%s".')
failure_message = _('Unable to update volumes for consistency group')
success_url = INDEX_URL
default_steps = (AddVolumesToCGroupStep,)
[docs] def handle(self, request, context):
cgroup_id = context['cgroup_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, 'consistencygroup_id'):
if volume.consistencygroup_id != cgroup_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, 'consistencygroup_id'):
if volume.consistencygroup_id == cgroup_id:
# remove from this CG
remove_vols.append(volume.id)
add_vols_str = ",".join(add_vols)
remove_vols_str = ",".join(remove_vols)
if not add_vols_str and not remove_vols_str:
# nothing to change
return True
cinder.volume_cgroup_update(request,
cgroup_id,
name=context['name'],
add_vols=add_vols_str,
remove_vols=remove_vols_str)
except Exception:
# error message supplied by form
return False
return True