Add Horizon support for volume consistency groups
Cinder has added support for creating and managing volume consistency groups. This first patch adds this functionality into Horizon. Subsequent patches will add features to utilize these consistency groups - such as for creating snapshots. Background/setup info: This feature provides a Horizon interface to the Cinder consistency groups (CG) API. CGs allow a user to group a set of volumes together, and then perform actions on all of the volumes in one command. For example: - Create CG snapshot. This will pause all volume I/O and create a snapshot for each volume in the CG at the same point in time. - Create CG from another CG. This will clone a new set of volumes based on the current state of all volumes in the original CG. - Create CG from CG snapshot. This will clone new set of volumes based on the saved state of all snapshots from the original CG snapshot. This patch is limited to just creating consistency groups. A second and final patch will add the snapshot features. To run the patch, you will need to enable some consistency group policies, which currently are defaulted to "none", which means they are disabled. The policies are set in /etc/cinder/policy.json - they need to be set to the following: "consistencygroup:create" : "rule:admin_or_owner", "consistencygroup:delete": "rule:admin_or_owner", "consistencygroup:update": "rule:admin_or_owner", "consistencygroup:get": "rule:admin_or_owner", "consistencygroup:get_all": "rule:admin_or_owner", "consistencygroup:create_cgsnapshot" : "rule:admin_or_owner", "consistencygroup:delete_cgsnapshot": "rule:admin_or_owner", "consistencygroup:get_cgsnapshot": "rule:admin_or_owner", "consistencygroup:get_all_cgsnapshots": "rule:admin_or_owner", Once this is done, you will need to restart all 3 of the cinder services in "screen -r" (c-api, c-sch, and c-vol). The new panel is a tab named "Volume Consistency Groups" and is located in "Project -> Volumes". Co-Authored-By: Brad Pokorny <brad_pokorny@symantec.com> Change-Id: I33ebe39e79d7c1d1dc7e741b4199bcb259b642d1 Partially-implements: blueprint cinder-consistency-groups
This commit is contained in:
parent
f071f9e3e7
commit
36c763168a
@ -80,8 +80,9 @@ class Volume(BaseCinderAPIResourceWrapper):
|
||||
_attrs = ['id', 'name', 'description', 'size', 'status', 'created_at',
|
||||
'volume_type', 'availability_zone', 'imageRef', 'bootable',
|
||||
'snapshot_id', 'source_volid', 'attachments', 'tenant_name',
|
||||
'os-vol-host-attr:host', 'os-vol-tenant-attr:tenant_id',
|
||||
'metadata', 'volume_image_metadata', 'encrypted', 'transfer']
|
||||
'consistencygroup_id', 'os-vol-host-attr:host',
|
||||
'os-vol-tenant-attr:tenant_id', 'metadata',
|
||||
'volume_image_metadata', 'encrypted', 'transfer']
|
||||
|
||||
@property
|
||||
def is_bootable(self):
|
||||
@ -102,6 +103,12 @@ class VolumeType(BaseCinderAPIResourceWrapper):
|
||||
'os-extended-snapshot-attributes:project_id']
|
||||
|
||||
|
||||
class VolumeConsistencyGroup(BaseCinderAPIResourceWrapper):
|
||||
|
||||
_attrs = ['id', 'name', 'description', 'status', 'availability_zone',
|
||||
'created_at', 'volume_types']
|
||||
|
||||
|
||||
class VolumeBackup(BaseCinderAPIResourceWrapper):
|
||||
|
||||
_attrs = ['id', 'name', 'description', 'container', 'size', 'status',
|
||||
@ -420,6 +427,59 @@ def volume_snapshot_reset_state(request, snapshot_id, state):
|
||||
snapshot_id, state)
|
||||
|
||||
|
||||
def volume_cgroup_get(request, cgroup_id):
|
||||
cgroup = cinderclient(request).consistencygroups.get(cgroup_id)
|
||||
return VolumeConsistencyGroup(cgroup)
|
||||
|
||||
|
||||
def volume_cgroup_list(request, search_opts=None):
|
||||
c_client = cinderclient(request)
|
||||
if c_client is None:
|
||||
return []
|
||||
return [VolumeConsistencyGroup(s) for s in c_client.consistencygroups.list(
|
||||
search_opts=search_opts)]
|
||||
|
||||
|
||||
def volume_cgroup_list_with_vol_type_names(request, search_opts=None):
|
||||
cgroups = volume_cgroup_list(request, search_opts)
|
||||
for cgroup in cgroups:
|
||||
cgroup.volume_type_names = []
|
||||
for vol_type_id in cgroup.volume_types:
|
||||
vol_type = volume_type_get(request, vol_type_id)
|
||||
cgroup.volume_type_names.append(vol_type.name)
|
||||
|
||||
return cgroups
|
||||
|
||||
|
||||
def volume_cgroup_create(request, volume_types, name,
|
||||
description=None, availability_zone=None):
|
||||
return VolumeConsistencyGroup(
|
||||
cinderclient(request).consistencygroups.create(
|
||||
volume_types,
|
||||
name,
|
||||
description,
|
||||
availability_zone=availability_zone))
|
||||
|
||||
|
||||
def volume_cgroup_delete(request, cgroup_id, force=False):
|
||||
return cinderclient(request).consistencygroups.delete(cgroup_id, force)
|
||||
|
||||
|
||||
def volume_cgroup_update(request, cgroup_id, name=None, description=None,
|
||||
add_vols=None, remove_vols=None):
|
||||
cgroup_data = {}
|
||||
if name:
|
||||
cgroup_data['name'] = name
|
||||
if description:
|
||||
cgroup_data['description'] = description
|
||||
if add_vols:
|
||||
cgroup_data['add_volumes'] = add_vols
|
||||
if remove_vols:
|
||||
cgroup_data['remove_volumes'] = remove_vols
|
||||
return cinderclient(request).consistencygroups.update(cgroup_id,
|
||||
**cgroup_data)
|
||||
|
||||
|
||||
@memoized
|
||||
def volume_backup_supported(request):
|
||||
"""This method will determine if cinder supports backup.
|
||||
|
@ -0,0 +1,47 @@
|
||||
# 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.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from openstack_dashboard.api import cinder
|
||||
|
||||
|
||||
class UpdateForm(forms.SelfHandlingForm):
|
||||
name = forms.CharField(max_length=255, label=_("Name"))
|
||||
description = forms.CharField(max_length=255,
|
||||
widget=forms.Textarea(attrs={'rows': 4}),
|
||||
label=_("Description"),
|
||||
required=False)
|
||||
|
||||
def handle(self, request, data):
|
||||
cgroup_id = self.initial['cgroup_id']
|
||||
try:
|
||||
cinder.volume_cgroup_update(request,
|
||||
cgroup_id,
|
||||
data['name'],
|
||||
data['description'])
|
||||
|
||||
message = _('Updating volume consistency '
|
||||
'group "%s"') % data['name']
|
||||
messages.info(request, message)
|
||||
return True
|
||||
except Exception:
|
||||
redirect = reverse("horizon:project:volumes:index")
|
||||
exceptions.handle(request,
|
||||
_('Unable to update volume consistency group.'),
|
||||
redirect=redirect)
|
149
openstack_dashboard/dashboards/project/volumes/cgroups/tables.py
Normal file
149
openstack_dashboard/dashboards/project/volumes/cgroups/tables.py
Normal file
@ -0,0 +1,149 @@
|
||||
# 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.core.urlresolvers import reverse
|
||||
from django.utils.translation import pgettext_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext_lazy
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard import policy
|
||||
|
||||
|
||||
class CreateVolumeCGroup(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
name = "create"
|
||||
verbose_name = _("Create Consistency Group")
|
||||
url = "horizon:project:volumes:cgroups:create"
|
||||
classes = ("ajax-modal",)
|
||||
icon = "plus"
|
||||
policy_rules = (("volume", "consistencygroup:create"),)
|
||||
|
||||
|
||||
class DeleteVolumeCGroup(policy.PolicyTargetMixin, tables.DeleteAction):
|
||||
name = "deletecg"
|
||||
policy_rules = (("volume", "consistencygroup:delete"), )
|
||||
|
||||
@staticmethod
|
||||
def action_present(count):
|
||||
return ungettext_lazy(
|
||||
u"Delete Consistency Group",
|
||||
u"Delete Consistency Groups",
|
||||
count
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def action_past(count):
|
||||
return ungettext_lazy(
|
||||
u"Scheduled deletion of Consistency Group",
|
||||
u"Scheduled deletion of Consistency Groups",
|
||||
count
|
||||
)
|
||||
|
||||
def delete(self, request, cgroup_id):
|
||||
try:
|
||||
cinder.volume_cgroup_delete(request,
|
||||
cgroup_id,
|
||||
force=False)
|
||||
except Exception:
|
||||
redirect = reverse("horizon:project:volumes:index")
|
||||
exceptions.handle(request,
|
||||
_('Unable to delete consistency group.'),
|
||||
redirect=redirect)
|
||||
|
||||
|
||||
class EditVolumeCGroup(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
name = "edit"
|
||||
verbose_name = _("Edit Consistency Group")
|
||||
url = "horizon:project:volumes:cgroups:update"
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "consistencygroup:update"),)
|
||||
|
||||
|
||||
class ManageVolumes(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
name = "manage"
|
||||
verbose_name = _("Manage Volumes")
|
||||
url = "horizon:project:volumes:cgroups:manage"
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "consistencygroup:update"),)
|
||||
|
||||
|
||||
class UpdateRow(tables.Row):
|
||||
ajax = True
|
||||
|
||||
def get_data(self, request, cgroup_id):
|
||||
cgroup = cinder.volume_cgroup_get(request, cgroup_id)
|
||||
return cgroup
|
||||
|
||||
|
||||
class VolumeCGroupsFilterAction(tables.FilterAction):
|
||||
|
||||
def filter(self, table, cgroups, filter_string):
|
||||
"""Naive case-insensitive search."""
|
||||
query = filter_string.lower()
|
||||
return [cgroup for cgroup in cgroups
|
||||
if query in cgroup.name.lower()]
|
||||
|
||||
|
||||
def get_volume_types(cgroup):
|
||||
vtypes_str = ",".join(cgroup.volume_type_names)
|
||||
return vtypes_str
|
||||
|
||||
|
||||
class VolumeCGroupsTable(tables.DataTable):
|
||||
STATUS_CHOICES = (
|
||||
("in-use", True),
|
||||
("available", True),
|
||||
("creating", None),
|
||||
("error", False),
|
||||
)
|
||||
STATUS_DISPLAY_CHOICES = (
|
||||
("available",
|
||||
pgettext_lazy("Current status of Consistency Group", u"Available")),
|
||||
("in-use",
|
||||
pgettext_lazy("Current status of Consistency Group", u"In-use")),
|
||||
("error",
|
||||
pgettext_lazy("Current status of Consistency Group", u"Error")),
|
||||
)
|
||||
|
||||
name = tables.Column("name",
|
||||
verbose_name=_("Name"),
|
||||
link="horizon:project:volumes:cgroups:detail")
|
||||
description = tables.Column("description",
|
||||
verbose_name=_("Description"),
|
||||
truncate=40)
|
||||
status = tables.Column("status",
|
||||
verbose_name=_("Status"),
|
||||
status=True,
|
||||
status_choices=STATUS_CHOICES,
|
||||
display_choices=STATUS_DISPLAY_CHOICES)
|
||||
availability_zone = tables.Column("availability_zone",
|
||||
verbose_name=_("Availability Zone"))
|
||||
volume_type = tables.Column(get_volume_types,
|
||||
verbose_name=_("Volume Type(s)"))
|
||||
|
||||
def get_object_id(self, cgroup):
|
||||
return cgroup.id
|
||||
|
||||
class Meta(object):
|
||||
name = "volume_cgroups"
|
||||
verbose_name = _("Volume Consistency Groups")
|
||||
table_actions = (CreateVolumeCGroup,
|
||||
VolumeCGroupsFilterAction)
|
||||
row_actions = (ManageVolumes,
|
||||
EditVolumeCGroup,
|
||||
DeleteVolumeCGroup)
|
||||
row_class = UpdateRow
|
||||
status_columns = ("status",)
|
||||
permissions = ['openstack.services.volume']
|
@ -0,0 +1,34 @@
|
||||
# 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.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tabs
|
||||
|
||||
|
||||
class OverviewTab(tabs.Tab):
|
||||
name = _("Overview")
|
||||
slug = "overview"
|
||||
template_name = ("project/volumes/cgroups/_detail_overview.html")
|
||||
|
||||
def get_context_data(self, request):
|
||||
cgroup = self.tab_group.kwargs['cgroup']
|
||||
return {"cgroup": cgroup}
|
||||
|
||||
def get_redirect_url(self):
|
||||
return reverse('horizon:project:volumes:index')
|
||||
|
||||
|
||||
class CGroupsDetailTabs(tabs.TabGroup):
|
||||
slug = "cgroup_details"
|
||||
tabs = (OverviewTab,)
|
234
openstack_dashboard/dashboards/project/volumes/cgroups/tests.py
Normal file
234
openstack_dashboard/dashboards/project/volumes/cgroups/tests.py
Normal file
@ -0,0 +1,234 @@
|
||||
# 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.core.urlresolvers import reverse
|
||||
from django import http
|
||||
from mox3.mox import IsA # noqa
|
||||
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
|
||||
VOLUME_INDEX_URL = reverse('horizon:project:volumes:index')
|
||||
VOLUME_CGROUPS_TAB_URL = reverse('horizon:project:volumes:cgroups_tab')
|
||||
|
||||
|
||||
class ConsistencyGroupTests(test.TestCase):
|
||||
@test.create_stubs({cinder: ('volume_cgroup_create',
|
||||
'volume_cgroup_list',
|
||||
'volume_type_list',
|
||||
'volume_type_list_with_qos_associations',
|
||||
'availability_zone_list',
|
||||
'extension_supported')})
|
||||
def test_create_cgroup(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
volume_types = self.volume_types.list()
|
||||
az = self.cinder_availability_zones.first().zoneName
|
||||
formData = {'volume_types': '1',
|
||||
'name': 'test CG',
|
||||
'description': 'test desc',
|
||||
'availability_zone': az}
|
||||
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(volume_types)
|
||||
cinder.volume_type_list_with_qos_associations(IsA(http.HttpRequest)).\
|
||||
AndReturn(volume_types)
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
cinder.volume_cgroup_list(IsA(
|
||||
http.HttpRequest)).\
|
||||
AndReturn(self.cinder_consistencygroups.list())
|
||||
cinder.volume_cgroup_create(
|
||||
IsA(http.HttpRequest),
|
||||
formData['volume_types'],
|
||||
formData['name'],
|
||||
formData['description'],
|
||||
availability_zone=formData['availability_zone'])\
|
||||
.AndReturn(cgroup)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:create')
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_create',
|
||||
'volume_cgroup_list',
|
||||
'volume_type_list',
|
||||
'volume_type_list_with_qos_associations',
|
||||
'availability_zone_list',
|
||||
'extension_supported')})
|
||||
def test_create_cgroup_exception(self):
|
||||
volume_types = self.volume_types.list()
|
||||
az = self.cinder_availability_zones.first().zoneName
|
||||
formData = {'volume_types': '1',
|
||||
'name': 'test CG',
|
||||
'description': 'test desc',
|
||||
'availability_zone': az}
|
||||
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(volume_types)
|
||||
cinder.volume_type_list_with_qos_associations(IsA(http.HttpRequest)).\
|
||||
AndReturn(volume_types)
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
cinder.volume_cgroup_list(IsA(
|
||||
http.HttpRequest)).\
|
||||
AndReturn(self.cinder_consistencygroups.list())
|
||||
cinder.volume_cgroup_create(
|
||||
IsA(http.HttpRequest),
|
||||
formData['volume_types'],
|
||||
formData['name'],
|
||||
formData['description'],
|
||||
availability_zone=formData['availability_zone'])\
|
||||
.AndRaise(self.exceptions.cinder)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:create')
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_list_with_vol_type_names',
|
||||
'volume_cgroup_delete')})
|
||||
def test_delete_cgroup(self):
|
||||
cgroups = self.cinder_consistencygroups.list()
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
|
||||
cinder.volume_cgroup_list_with_vol_type_names(IsA(http.HttpRequest)).\
|
||||
AndReturn(cgroups)
|
||||
cinder.volume_cgroup_delete(IsA(http.HttpRequest), cgroup.id,
|
||||
force=False)
|
||||
cinder.volume_cgroup_list_with_vol_type_names(IsA(http.HttpRequest)).\
|
||||
AndReturn(cgroups)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
formData = {'action': 'volume_cgroups__deletecg__%s' % cgroup.id}
|
||||
res = self.client.post(VOLUME_CGROUPS_TAB_URL, formData, follow=True)
|
||||
self.assertIn("Scheduled deletion of Consistency Group: cg_1",
|
||||
[m.message for m in res.context['messages']])
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_update',
|
||||
'volume_cgroup_get')})
|
||||
def test_update_cgroup_add_vol(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
volume = self.cinder_volumes.first()
|
||||
formData = {'volume_types': '1',
|
||||
'name': 'test CG',
|
||||
'description': 'test desc'}
|
||||
|
||||
cinder.volume_cgroup_get(IsA(
|
||||
http.HttpRequest), cgroup.id).\
|
||||
AndReturn(cgroup)
|
||||
cinder.volume_cgroup_update(
|
||||
IsA(http.HttpRequest),
|
||||
formData['name'],
|
||||
formData['description'],
|
||||
add_vols=volume)\
|
||||
.AndReturn(cgroup)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:update',
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_update',
|
||||
'volume_cgroup_get')})
|
||||
def test_update_cgroup_remove_vol(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
volume = self.cinder_volumes.first()
|
||||
formData = {'volume_types': '1',
|
||||
'name': 'test CG',
|
||||
'description': 'test desc'}
|
||||
|
||||
cinder.volume_cgroup_get(IsA(
|
||||
http.HttpRequest), cgroup.id).\
|
||||
AndReturn(cgroup)
|
||||
cinder.volume_cgroup_update(
|
||||
IsA(http.HttpRequest),
|
||||
formData['name'],
|
||||
formData['description'],
|
||||
remove_vols=volume)\
|
||||
.AndReturn(cgroup)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:update',
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_update',
|
||||
'volume_cgroup_get')})
|
||||
def test_update_cgroup_name_and_description(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
formData = {'volume_types': '1',
|
||||
'name': 'test CG-new',
|
||||
'description': 'test desc-new'}
|
||||
|
||||
cinder.volume_cgroup_get(IsA(
|
||||
http.HttpRequest), cgroup.id).\
|
||||
AndReturn(cgroup)
|
||||
cinder.volume_cgroup_update(
|
||||
IsA(http.HttpRequest),
|
||||
formData['name'],
|
||||
formData['description'])\
|
||||
.AndReturn(cgroup)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:update',
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_update',
|
||||
'volume_cgroup_get')})
|
||||
def test_update_cgroup_with_exception(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
formData = {'volume_types': '1',
|
||||
'name': 'test CG-new',
|
||||
'description': 'test desc-new'}
|
||||
|
||||
cinder.volume_cgroup_get(IsA(
|
||||
http.HttpRequest), cgroup.id).\
|
||||
AndReturn(cgroup)
|
||||
cinder.volume_cgroup_update(
|
||||
IsA(http.HttpRequest),
|
||||
formData['name'],
|
||||
formData['description'])\
|
||||
.AndRaise(self.exceptions.cinder)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:update',
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_get',)})
|
||||
def test_detail_view_with_exception(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
|
||||
cinder.volume_cgroup_get(IsA(http.HttpRequest), cgroup.id).\
|
||||
AndRaise(self.exceptions.cinder)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:detail',
|
||||
args=[cgroup.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
@ -0,0 +1,33 @@
|
||||
# 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.urls import patterns
|
||||
from django.conf.urls import url
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes.cgroups import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^create/$',
|
||||
views.CreateView.as_view(),
|
||||
name='create'),
|
||||
url(r'^(?P<cgroup_id>[^/]+)/update/$',
|
||||
views.UpdateView.as_view(),
|
||||
name='update'),
|
||||
url(r'^(?P<cgroup_id>[^/]+)/manage/$',
|
||||
views.ManageView.as_view(),
|
||||
name='manage'),
|
||||
url(r'^(?P<cgroup_id>[^/]+)$',
|
||||
views.DetailView.as_view(),
|
||||
name='detail'),
|
||||
)
|
155
openstack_dashboard/dashboards/project/volumes/cgroups/views.py
Normal file
155
openstack_dashboard/dashboards/project/volumes/cgroups/views.py
Normal file
@ -0,0 +1,155 @@
|
||||
# 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.core.urlresolvers import reverse
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tabs
|
||||
from horizon.utils import memoized
|
||||
from horizon import workflows
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.cgroups import workflows as vol_cgroup_workflows
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.cgroups import forms as vol_cgroup_forms
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.cgroups import tables as vol_cgroup_tables
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.cgroups import tabs as vol_cgroup_tabs
|
||||
|
||||
CGROUP_INFO_FIELDS = ("name",
|
||||
"description")
|
||||
|
||||
INDEX_URL = "horizon:project:volumes:index"
|
||||
|
||||
|
||||
class CreateView(workflows.WorkflowView):
|
||||
workflow_class = vol_cgroup_workflows.CreateCGroupWorkflow
|
||||
template_name = 'project/volumes/cgroups/create.html'
|
||||
page_title = _("Create Volume Consistency Group")
|
||||
|
||||
|
||||
class UpdateView(forms.ModalFormView):
|
||||
template_name = 'project/volumes/cgroups/update.html'
|
||||
modal_header = _("Edit Consistency Group")
|
||||
form_class = vol_cgroup_forms.UpdateForm
|
||||
success_url = reverse_lazy('horizon:project:volumes:index')
|
||||
submit_url = "horizon:project:volumes:cgroups:update"
|
||||
submit_label = modal_header
|
||||
page_title = modal_header
|
||||
|
||||
def get_initial(self):
|
||||
cgroup = self.get_object()
|
||||
return {'cgroup_id': self.kwargs["cgroup_id"],
|
||||
'name': cgroup.name,
|
||||
'description': cgroup.description}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UpdateView, self).get_context_data(**kwargs)
|
||||
context['cgroup_id'] = self.kwargs['cgroup_id']
|
||||
args = (self.kwargs['cgroup_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
return context
|
||||
|
||||
def get_object(self):
|
||||
cgroup_id = self.kwargs['cgroup_id']
|
||||
try:
|
||||
self._object = cinder.volume_cgroup_get(self.request, cgroup_id)
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve consistency group '
|
||||
'details.'),
|
||||
redirect=reverse(INDEX_URL))
|
||||
return self._object
|
||||
|
||||
|
||||
class ManageView(workflows.WorkflowView):
|
||||
workflow_class = vol_cgroup_workflows.UpdateCGroupWorkflow
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ManageView, self).get_context_data(**kwargs)
|
||||
context['cgroup_id'] = self.kwargs["cgroup_id"]
|
||||
return context
|
||||
|
||||
def _get_object(self, *args, **kwargs):
|
||||
cgroup_id = self.kwargs['cgroup_id']
|
||||
try:
|
||||
cgroup = cinder.volume_cgroup_get(self.request, cgroup_id)
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve consistency group '
|
||||
'details.'),
|
||||
redirect=reverse(INDEX_URL))
|
||||
return cgroup
|
||||
|
||||
def get_initial(self):
|
||||
cgroup = self._get_object()
|
||||
return {'cgroup_id': cgroup.id,
|
||||
'name': cgroup.name,
|
||||
'description': cgroup.description,
|
||||
'vtypes': getattr(cgroup, "volume_types")}
|
||||
|
||||
|
||||
class DetailView(tabs.TabView):
|
||||
tab_group_class = vol_cgroup_tabs.CGroupsDetailTabs
|
||||
template_name = 'horizon/common/_detail.html'
|
||||
page_title = "{{ cgroup.name|default:cgroup.id }}"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
cgroup = self.get_data()
|
||||
table = vol_cgroup_tables.VolumeCGroupsTable(self.request)
|
||||
context["cgroup"] = cgroup
|
||||
context["url"] = self.get_redirect_url()
|
||||
context["actions"] = table.render_row_actions(cgroup)
|
||||
return context
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self):
|
||||
try:
|
||||
cgroup_id = self.kwargs['cgroup_id']
|
||||
cgroup = api.cinder.volume_cgroup_get(self.request,
|
||||
cgroup_id)
|
||||
cgroup.volume_type_names = []
|
||||
for vol_type_id in cgroup.volume_types:
|
||||
vol_type = api.cinder.volume_type_get(self.request,
|
||||
vol_type_id)
|
||||
cgroup.volume_type_names.append(vol_type.name)
|
||||
|
||||
cgroup.volume_names = []
|
||||
search_opts = {'consistencygroup_id': cgroup_id}
|
||||
volumes = api.cinder.volume_list(self.request,
|
||||
search_opts=search_opts)
|
||||
for volume in volumes:
|
||||
cgroup.volume_names.append(volume.name)
|
||||
|
||||
except Exception:
|
||||
redirect = self.get_redirect_url()
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve consistency group '
|
||||
'details.'),
|
||||
redirect=redirect)
|
||||
return cgroup
|
||||
|
||||
@staticmethod
|
||||
def get_redirect_url():
|
||||
return reverse('horizon:project:volumes:index')
|
||||
|
||||
def get_tabs(self, request, *args, **kwargs):
|
||||
cgroup = self.get_data()
|
||||
return self.tab_group_class(request, cgroup=cgroup, **kwargs)
|
@ -0,0 +1,423 @@
|
||||
# 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 messages
|
||||
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"
|
||||
|
||||
|
||||
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 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"
|
||||
|
||||
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():
|
||||
raise forms.ValidationError(
|
||||
_('The name "%s" is already used by '
|
||||
'another consistency group.')
|
||||
% name
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class AddCGroupInfoStep(workflows.Step):
|
||||
action_class = AddCGroupInfoAction
|
||||
contributes = ("availability_zone",
|
||||
"description",
|
||||
"name")
|
||||
|
||||
|
||||
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_names = []
|
||||
for vtype in vtypes:
|
||||
if vtype.name not in vtype_names:
|
||||
vtype_names.append(vtype.name)
|
||||
vtype_names.sort()
|
||||
|
||||
self.fields[field_name].choices = \
|
||||
[(vtype_name, vtype_name) for vtype_name in vtype_names]
|
||||
|
||||
class Meta(object):
|
||||
name = _("Manage Volume Types")
|
||||
slug = "add_vtypes_to_cgroup"
|
||||
|
||||
|
||||
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",)
|
||||
|
||||
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 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:
|
||||
in_this_cgroup = False
|
||||
if hasattr(volume, 'consistencygroup_id'):
|
||||
if volume.consistencygroup_id == \
|
||||
self.initial['cgroup_id']:
|
||||
in_this_cgroup = True
|
||||
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((entry, entry))
|
||||
if volume['in_cgroup']:
|
||||
assigned_vols.append(entry)
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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",)
|
||||
|
||||
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 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)
|
||||
|
||||
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.name:
|
||||
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,
|
||||
name=context['name'],
|
||||
description=context['description'],
|
||||
availability_zone=context['availability_zone'])
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Unable to create consistency '
|
||||
'group.'))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class UpdateCGroupWorkflow(workflows.Workflow):
|
||||
slug = "create_cgroup"
|
||||
name = _("Add/Remove Consistency Group Volumes")
|
||||
finalize_button_name = _("Edit Consistency Group")
|
||||
success_message = _('Edit consistency group "%s".')
|
||||
failure_message = _('Unable to edit consistency group')
|
||||
success_url = INDEX_URL
|
||||
default_steps = (AddVolumesToCGroupStep,)
|
||||
|
||||
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 " [" in selection:
|
||||
# handle duplicate volume names
|
||||
sel = selection.split(" [")
|
||||
sel_vol_name = sel[0]
|
||||
sel_vol_id = sel[1].split("]")[0]
|
||||
else:
|
||||
sel_vol_name = selection
|
||||
sel_vol_id = None
|
||||
|
||||
if volume.name == sel_vol_name:
|
||||
if sel_vol_id:
|
||||
if sel_vol_id == volume.id:
|
||||
selected = True
|
||||
else:
|
||||
selected = True
|
||||
|
||||
if selected:
|
||||
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_vols.append(volume.id)
|
||||
|
||||
add_vols_str = ",".join(add_vols)
|
||||
remove_vols_str = ",".join(remove_vols)
|
||||
cinder.volume_cgroup_update(request,
|
||||
cgroup_id,
|
||||
name=context['name'],
|
||||
add_vols=add_vols_str,
|
||||
remove_vols=remove_vols_str)
|
||||
|
||||
# before returning, ensure all new volumes are correctly assigned
|
||||
self._verify_changes(request, cgroup_id, add_vols, remove_vols)
|
||||
|
||||
message = _('Updating volume consistency '
|
||||
'group "%s"') % context['name']
|
||||
messages.info(request, message)
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Unable to edit consistency group.'))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _verify_changes(self, request, cgroup_id, add_vols, remove_vols):
|
||||
search_opts = {'consistencygroup_id': cgroup_id}
|
||||
done = False
|
||||
while not done:
|
||||
done = True
|
||||
volumes = cinder.volume_list(request,
|
||||
search_opts=search_opts)
|
||||
assigned_vols = []
|
||||
for volume in volumes:
|
||||
assigned_vols.append(volume.id)
|
||||
|
||||
for add_vol in add_vols:
|
||||
if add_vol not in assigned_vols:
|
||||
done = False
|
||||
|
||||
for remove_vol in remove_vols:
|
||||
if remove_vol in assigned_vols:
|
||||
done = False
|
@ -20,9 +20,12 @@ from horizon import exceptions
|
||||
from horizon import tabs
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard import policy
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes.backups \
|
||||
import tables as backups_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.cgroups \
|
||||
import tables as vol_cgroup_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.snapshots \
|
||||
import tables as vol_snapshot_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.volumes \
|
||||
@ -203,7 +206,33 @@ class BackupsTab(PagedTableMixin, tabs.TableTab, VolumeTableMixIn):
|
||||
return backups
|
||||
|
||||
|
||||
class CGroupsTab(tabs.TableTab, VolumeTableMixIn):
|
||||
table_classes = (vol_cgroup_tables.VolumeCGroupsTable,)
|
||||
name = _("Volume Consistency Groups")
|
||||
slug = "cgroups_tab"
|
||||
template_name = ("horizon/common/_detail_table.html")
|
||||
preload = False
|
||||
|
||||
def allowed(self, request):
|
||||
return policy.check(
|
||||
(("volume", "consistencygroup:get_all"),),
|
||||
request
|
||||
)
|
||||
|
||||
def get_volume_cgroups_data(self):
|
||||
try:
|
||||
cgroups = api.cinder.volume_cgroup_list_with_vol_type_names(
|
||||
self.request)
|
||||
for cgroup in cgroups:
|
||||
setattr(cgroup, '_volume_tab', self.tab_group.tabs[0])
|
||||
except Exception:
|
||||
cgroups = []
|
||||
exceptions.handle(self.request, _("Unable to retrieve "
|
||||
"volume consistency groups."))
|
||||
return cgroups
|
||||
|
||||
|
||||
class VolumeAndSnapshotTabs(tabs.TabGroup):
|
||||
slug = "volumes_and_snapshots"
|
||||
tabs = (VolumeTab, SnapshotTab, BackupsTab)
|
||||
tabs = (VolumeTab, SnapshotTab, BackupsTab, CGroupsTab)
|
||||
sticky = True
|
||||
|
@ -0,0 +1,10 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block modal-body-right %}
|
||||
<p>{% trans "Volume consistency groups can only be deleted after all the volumes they contain are either deleted or unassigned." %}</p>
|
||||
<p>{% trans "The default action for deleting a consistency group is to first disassociate all associated volumes." %}</p>
|
||||
<p>{% trans "Check the "Delete Volumes" box to also delete the volumes associated with this consistency group." %}</p>
|
||||
<p>{% trans "Note that a volume can not be deleted if it is "attached" or has any dependent snapshots." %}</p>
|
||||
{% endblock %}
|
@ -0,0 +1,34 @@
|
||||
{% load i18n sizeformat parse_date %}
|
||||
|
||||
<div class="detail">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ cgroup.name }}</dd>
|
||||
<dt>{% trans "ID" %}</dt>
|
||||
<dd>{{ cgroup.id }}</dd>
|
||||
<dt>{% trans "Description" %}</dt>
|
||||
<dd>{{ cgroup.description }}</dd>
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>{{ cgroup.status|capfirst }}</dd>
|
||||
</dl>
|
||||
|
||||
<h4>{% trans "Volume Types" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl class="dl-horizontal">
|
||||
{% for vol_type_names in cgroup.volume_type_names %}
|
||||
<dd>{{ vol_type_names }}</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
|
||||
<h4>{% trans "Volumes" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl class="dl-horizontal">
|
||||
{% for vol_names in cgroup.volume_names %}
|
||||
<dd>{{ vol_names }}</dd>
|
||||
{% empty %}
|
||||
<dd>
|
||||
<em>{% trans "No assigned volumes" %}</em>
|
||||
</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
@ -0,0 +1,7 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block modal-body-right %}
|
||||
<p>{% trans "Modify the name and description of a volume consistency group." %}</p>
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'horizon/common/_workflow.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/cgroups/_delete.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/cgroups/_update.html' %}
|
||||
{% endblock %}
|
@ -18,6 +18,8 @@ from django.conf.urls import url
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes.backups \
|
||||
import urls as backups_urls
|
||||
from openstack_dashboard.dashboards.project.volumes.cgroups \
|
||||
import urls as cgroup_urls
|
||||
from openstack_dashboard.dashboards.project.volumes.snapshots \
|
||||
import urls as snapshot_urls
|
||||
from openstack_dashboard.dashboards.project.volumes import views
|
||||
@ -33,7 +35,10 @@ urlpatterns = patterns(
|
||||
views.IndexView.as_view(), name='volumes_tab'),
|
||||
url(r'^\?tab=volumes_and_snapshots__backups_tab$',
|
||||
views.IndexView.as_view(), name='backups_tab'),
|
||||
url(r'^\?tab=volumes_and_snapshots__cgroups_tab$',
|
||||
views.IndexView.as_view(), name='cgroups_tab'),
|
||||
url(r'', include(volume_urls, namespace='volumes')),
|
||||
url(r'backups/', include(backups_urls, namespace='backups')),
|
||||
url(r'snapshots/', include(snapshot_urls, namespace='snapshots')),
|
||||
url(r'cgroups/', include(cgroup_urls, namespace='cgroups')),
|
||||
)
|
||||
|
@ -333,6 +333,46 @@ class CinderApiTests(test.APITestCase):
|
||||
default_volume_type = api.cinder.volume_type_default(self.request)
|
||||
self.assertEqual(default_volume_type, volume_type)
|
||||
|
||||
def test_cgroup_list(self):
|
||||
cgroups = self.cinder_consistencygroups.list()
|
||||
cinderclient = self.stub_cinderclient()
|
||||
cinderclient.consistencygroups = self.mox.CreateMockAnything()
|
||||
cinderclient.consistencygroups.list(search_opts=None).\
|
||||
AndReturn(cgroups)
|
||||
self.mox.ReplayAll()
|
||||
api_cgroups = api.cinder.volume_cgroup_list(self.request)
|
||||
self.assertEqual(len(cgroups), len(api_cgroups))
|
||||
|
||||
def test_cgroup_get(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
cinderclient = self.stub_cinderclient()
|
||||
cinderclient.consistencygroups = self.mox.CreateMockAnything()
|
||||
cinderclient.consistencygroups.get(cgroup.id).AndReturn(cgroup)
|
||||
self.mox.ReplayAll()
|
||||
api_cgroup = api.cinder.volume_cgroup_get(self.request, cgroup.id)
|
||||
self.assertEqual(api_cgroup.name, cgroup.name)
|
||||
self.assertEqual(api_cgroup.description, cgroup.description)
|
||||
self.assertEqual(api_cgroup.volume_types, cgroup.volume_types)
|
||||
|
||||
def test_cgroup_list_with_vol_type_names(self):
|
||||
cgroups = self.cinder_consistencygroups.list()
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
volume_types_list = self.cinder_volume_types.list()
|
||||
cinderclient = self.stub_cinderclient()
|
||||
cinderclient.consistencygroups = self.mox.CreateMockAnything()
|
||||
cinderclient.consistencygroups.list(search_opts=None).\
|
||||
AndReturn(cgroups)
|
||||
cinderclient.volume_types = self.mox.CreateMockAnything()
|
||||
for volume_types in volume_types_list:
|
||||
cinderclient.volume_types.get(cgroup.id).AndReturn(volume_types)
|
||||
self.mox.ReplayAll()
|
||||
api_cgroups = api.cinder.volume_cgroup_list_with_vol_type_names(
|
||||
self.request)
|
||||
self.assertEqual(len(cgroups), len(api_cgroups))
|
||||
for i in range(len(api_cgroups[0].volume_type_names)):
|
||||
self.assertEqual(volume_types_list[i].name,
|
||||
api_cgroups[0].volume_type_names[i])
|
||||
|
||||
|
||||
class CinderApiVersionTests(test.TestCase):
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
# under the License.
|
||||
|
||||
from cinderclient.v2 import availability_zones
|
||||
from cinderclient.v2 import consistencygroups
|
||||
from cinderclient.v2 import pools
|
||||
from cinderclient.v2 import qos_specs
|
||||
from cinderclient.v2 import quotas
|
||||
@ -46,6 +47,8 @@ def data(TEST):
|
||||
TEST.cinder_availability_zones = utils.TestDataContainer()
|
||||
TEST.cinder_volume_transfers = utils.TestDataContainer()
|
||||
TEST.cinder_pools = utils.TestDataContainer()
|
||||
TEST.cinder_consistencygroups = utils.TestDataContainer()
|
||||
TEST.cinder_cgroup_volumes = utils.TestDataContainer()
|
||||
|
||||
# Services
|
||||
service_1 = services.Service(services.ServiceManager(None), {
|
||||
@ -382,3 +385,37 @@ def data(TEST):
|
||||
|
||||
TEST.cinder_pools.add(pool1)
|
||||
TEST.cinder_pools.add(pool2)
|
||||
|
||||
# volume consistency groups
|
||||
cgroup_1 = consistencygroups.Consistencygroup(
|
||||
consistencygroups.ConsistencygroupManager(None),
|
||||
{'id': u'1',
|
||||
'name': u'cg_1',
|
||||
'description': 'cg 1 description',
|
||||
'volume_types': u'1',
|
||||
'volume_type_names': []})
|
||||
|
||||
cgroup_2 = consistencygroups.Consistencygroup(
|
||||
consistencygroups.ConsistencygroupManager(None),
|
||||
{'id': u'2',
|
||||
'name': u'cg_2',
|
||||
'description': 'cg 2 description',
|
||||
'volume_types': u'1',
|
||||
'volume_type_names': []})
|
||||
|
||||
TEST.cinder_consistencygroups.add(cgroup_1)
|
||||
TEST.cinder_consistencygroups.add(cgroup_2)
|
||||
|
||||
volume_for_consistency_group = volumes.Volume(
|
||||
volumes.VolumeManager(None),
|
||||
{'id': "11023e92-8008-4c8b-8059-7f2293ff3881",
|
||||
'status': 'available',
|
||||
'size': 40,
|
||||
'display_name': 'Volume name',
|
||||
'display_description': 'Volume description',
|
||||
'created_at': '2014-01-27 10:30:00',
|
||||
'volume_type': None,
|
||||
'attachments': [],
|
||||
'consistencygroup_id': u'1'})
|
||||
TEST.cinder_cgroup_volumes.add(api.cinder.Volume(
|
||||
volume_for_consistency_group))
|
||||
|
@ -0,0 +1,12 @@
|
||||
---
|
||||
features:
|
||||
- >
|
||||
[`blueprint cinder-consistency-groups <https://blueprints.launchpad.net/horizon/+spec/cinder-consistency-groups>`_]
|
||||
This feature adds a new Consistency Groups tab to the Project Volumes panel.
|
||||
Consistency Groups (GG) contain existing volumes, and allow the user to perform
|
||||
actions on the volumes in one step. Actions include: create a CG, manage volumes
|
||||
associated with the CG, update a CG, and delete a CGs. Note that a CG can not be
|
||||
deleted if it contains any volumes.
|
||||
security:
|
||||
- Policies associated with Consistency Groups exist in the Cinder policy file, and
|
||||
by default, all actions are disabled.
|
Loading…
Reference in New Issue
Block a user