Add support for volume consistency group snapshots
This extends the support for Cinder volume consistency groups. This patch adds snapshot support for all volumes contained within a consistency group. Additional features includes the ability to create a new consistency group from an existing consistency group or consistency group snapshot. This will involve cloning all volumes in the original CG. To enable this feature, update "consistencygroup" items in the cinder policy file (/etc/cinder/policy.json). The default is that all consistencygroup actions are disabled. Co-Authored-By: Brad Pokorny <brad_pokorny@symantec.com> Change-Id: I2e2ae26369a23415bf58d6edac8d161d3a125dd6 Implements: blueprint cinder-consistency-groups
This commit is contained in:
parent
7df4449e2c
commit
b1a22463d6
@ -109,6 +109,12 @@ class VolumeConsistencyGroup(BaseCinderAPIResourceWrapper):
|
||||
'created_at', 'volume_types']
|
||||
|
||||
|
||||
class VolumeCGSnapshot(BaseCinderAPIResourceWrapper):
|
||||
|
||||
_attrs = ['id', 'name', 'description', 'status',
|
||||
'created_at', 'consistencygroup_id']
|
||||
|
||||
|
||||
class VolumeBackup(BaseCinderAPIResourceWrapper):
|
||||
|
||||
_attrs = ['id', 'name', 'description', 'container', 'size', 'status',
|
||||
@ -432,6 +438,18 @@ def volume_cgroup_get(request, cgroup_id):
|
||||
return VolumeConsistencyGroup(cgroup)
|
||||
|
||||
|
||||
def volume_cgroup_get_with_vol_type_names(request, cgroup_id):
|
||||
cgroup = volume_cgroup_get(request, cgroup_id)
|
||||
vol_types = volume_type_list(request)
|
||||
cgroup.volume_type_names = []
|
||||
for vol_type_id in cgroup.volume_types:
|
||||
for vol_type in vol_types:
|
||||
if vol_type.id == vol_type_id:
|
||||
cgroup.volume_type_names.append(vol_type.name)
|
||||
break
|
||||
return cgroup
|
||||
|
||||
|
||||
def volume_cgroup_list(request, search_opts=None):
|
||||
c_client = cinderclient(request)
|
||||
if c_client is None:
|
||||
@ -442,23 +460,41 @@ def volume_cgroup_list(request, search_opts=None):
|
||||
|
||||
def volume_cgroup_list_with_vol_type_names(request, search_opts=None):
|
||||
cgroups = volume_cgroup_list(request, search_opts)
|
||||
vol_types = volume_type_list(request)
|
||||
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)
|
||||
for vol_type in vol_types:
|
||||
if vol_type.id == vol_type_id:
|
||||
cgroup.volume_type_names.append(vol_type.name)
|
||||
break
|
||||
|
||||
return cgroups
|
||||
|
||||
|
||||
def volume_cgroup_create(request, volume_types, name,
|
||||
description=None, availability_zone=None):
|
||||
data = {'name': name,
|
||||
'description': description,
|
||||
'availability_zone': availability_zone}
|
||||
|
||||
cgroup = cinderclient(request).consistencygroups.create(volume_types,
|
||||
**data)
|
||||
return VolumeConsistencyGroup(cgroup)
|
||||
|
||||
|
||||
def volume_cgroup_create_from_source(request, name, cg_snapshot_id=None,
|
||||
source_cgroup_id=None,
|
||||
description=None,
|
||||
user_id=None, project_id=None):
|
||||
return VolumeConsistencyGroup(
|
||||
cinderclient(request).consistencygroups.create(
|
||||
volume_types,
|
||||
cinderclient(request).consistencygroups.create_from_src(
|
||||
cg_snapshot_id,
|
||||
source_cgroup_id,
|
||||
name,
|
||||
description,
|
||||
availability_zone=availability_zone))
|
||||
user_id,
|
||||
project_id))
|
||||
|
||||
|
||||
def volume_cgroup_delete(request, cgroup_id, force=False):
|
||||
@ -480,6 +516,32 @@ def volume_cgroup_update(request, cgroup_id, name=None, description=None,
|
||||
**cgroup_data)
|
||||
|
||||
|
||||
def volume_cg_snapshot_create(request, cgroup_id, name,
|
||||
description=None):
|
||||
return VolumeCGSnapshot(
|
||||
cinderclient(request).cgsnapshots.create(
|
||||
cgroup_id,
|
||||
name,
|
||||
description))
|
||||
|
||||
|
||||
def volume_cg_snapshot_get(request, cg_snapshot_id):
|
||||
cgsnapshot = cinderclient(request).cgsnapshots.get(cg_snapshot_id)
|
||||
return VolumeCGSnapshot(cgsnapshot)
|
||||
|
||||
|
||||
def volume_cg_snapshot_list(request, search_opts=None):
|
||||
c_client = cinderclient(request)
|
||||
if c_client is None:
|
||||
return []
|
||||
return [VolumeCGSnapshot(s) for s in c_client.cgsnapshots.list(
|
||||
search_opts=search_opts)]
|
||||
|
||||
|
||||
def volume_cg_snapshot_delete(request, cg_snapshot_id):
|
||||
return cinderclient(request).cgsnapshots.delete(cg_snapshot_id)
|
||||
|
||||
|
||||
@memoized
|
||||
def volume_backup_supported(request):
|
||||
"""This method will determine if cinder supports backup.
|
||||
|
@ -0,0 +1,76 @@
|
||||
# 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 CreateCGroupForm(forms.SelfHandlingForm):
|
||||
name = forms.CharField(max_length=255, label=_("Consistency Group Name"))
|
||||
description = forms.CharField(max_length=255,
|
||||
widget=forms.Textarea(attrs={'rows': 4}),
|
||||
label=_("Description"),
|
||||
required=False)
|
||||
snapshot_source = forms.ChoiceField(
|
||||
label=_("Use snapshot as a source"),
|
||||
widget=forms.SelectWidget(
|
||||
attrs={'class': 'snapshot-selector'},
|
||||
data_attrs=('name'),
|
||||
transform=lambda x: "%s" % (x.name)),
|
||||
required=False)
|
||||
|
||||
def prepare_snapshot_source_field(self, request, cg_snapshot_id):
|
||||
try:
|
||||
cg_snapshot = cinder.volume_cg_snapshot_get(request,
|
||||
cg_snapshot_id)
|
||||
self.fields['snapshot_source'].choices = ((cg_snapshot_id,
|
||||
cg_snapshot),)
|
||||
except Exception:
|
||||
exceptions.handle(request,
|
||||
_('Unable to load the specified snapshot.'))
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(CreateCGroupForm, self).__init__(request, *args, **kwargs)
|
||||
|
||||
# populate cgroup_id
|
||||
cg_snapshot_id = kwargs.get('initial', {}).get('cg_snapshot_id', [])
|
||||
self.fields['cg_snapshot_id'] = forms.CharField(
|
||||
widget=forms.HiddenInput(),
|
||||
initial=cg_snapshot_id)
|
||||
self.prepare_snapshot_source_field(request, cg_snapshot_id)
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
|
||||
message = _('Creating consistency group "%s".') % data['name']
|
||||
cgroup = cinder.volume_cgroup_create_from_source(
|
||||
request,
|
||||
data['name'],
|
||||
cg_snapshot_id=data['cg_snapshot_id'],
|
||||
description=data['description'])
|
||||
|
||||
messages.info(request, message)
|
||||
return cgroup
|
||||
except Exception:
|
||||
redirect = reverse("horizon:project:volumes:index")
|
||||
msg = _('Unable to create consistency '
|
||||
'group "%s" from snapshot.') % data['name']
|
||||
exceptions.handle(request,
|
||||
msg,
|
||||
redirect=redirect)
|
@ -0,0 +1,116 @@
|
||||
# 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 pgettext_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext_lazy
|
||||
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard import policy
|
||||
|
||||
|
||||
class CreateVolumeCGroup(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
name = "create_cgroup"
|
||||
verbose_name = _("Create Consistency Group")
|
||||
url = "horizon:project:volumes:cg_snapshots:create_cgroup"
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "consistencygroup:create"),)
|
||||
|
||||
|
||||
class DeleteVolumeCGSnapshot(policy.PolicyTargetMixin, tables.DeleteAction):
|
||||
name = "delete_cg_snapshot"
|
||||
policy_rules = (("volume", "consistencygroup:delete_cgsnapshot"),)
|
||||
|
||||
@staticmethod
|
||||
def action_present(count):
|
||||
return ungettext_lazy(
|
||||
u"Delete Snapshot",
|
||||
u"Delete Snapshots",
|
||||
count
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def action_past(count):
|
||||
return ungettext_lazy(
|
||||
u"Scheduled deletion of Snapshot",
|
||||
u"Scheduled deletion of Snapshots",
|
||||
count
|
||||
)
|
||||
|
||||
def delete(self, request, obj_id):
|
||||
cinder.volume_cg_snapshot_delete(request, obj_id)
|
||||
|
||||
|
||||
class UpdateRow(tables.Row):
|
||||
ajax = True
|
||||
|
||||
def get_data(self, request, cg_snapshot_id):
|
||||
cg_snapshot = cinder.volume_cg_snapshot_get(request, cg_snapshot_id)
|
||||
return cg_snapshot
|
||||
|
||||
|
||||
class VolumeCGSnapshotsFilterAction(tables.FilterAction):
|
||||
|
||||
def filter(self, table, cg_snapshots, filter_string):
|
||||
"""Naive case-insensitive search."""
|
||||
query = filter_string.lower()
|
||||
return [cg_snapshot for cg_snapshot in cg_snapshots
|
||||
if query in cg_snapshot.name.lower()]
|
||||
|
||||
|
||||
class CGSnapshotsTable(tables.DataTable):
|
||||
STATUS_CHOICES = (
|
||||
("in-use", True),
|
||||
("available", True),
|
||||
("creating", None),
|
||||
("error", False),
|
||||
)
|
||||
STATUS_DISPLAY_CHOICES = (
|
||||
("available",
|
||||
pgettext_lazy("Current status of Consistency Group Snapshot",
|
||||
u"Available")),
|
||||
("in-use",
|
||||
pgettext_lazy("Current status of Consistency Group Snapshot",
|
||||
u"In-use")),
|
||||
("error",
|
||||
pgettext_lazy("Current status of Consistency Group Snapshot",
|
||||
u"Error")),
|
||||
)
|
||||
|
||||
name = tables.Column("name",
|
||||
verbose_name=_("Name"),
|
||||
link="horizon:project:volumes:"
|
||||
"cg_snapshots:cg_snapshot_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)
|
||||
|
||||
def get_object_id(self, cg_snapshot):
|
||||
return cg_snapshot.id
|
||||
|
||||
class Meta(object):
|
||||
name = "volume_cg_snapshots"
|
||||
verbose_name = _("Consistency Group Snapshots")
|
||||
table_actions = (VolumeCGSnapshotsFilterAction,
|
||||
DeleteVolumeCGSnapshot)
|
||||
row_actions = (CreateVolumeCGroup,
|
||||
DeleteVolumeCGSnapshot,)
|
||||
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/cg_snapshots/_detail_overview.html")
|
||||
|
||||
def get_context_data(self, request):
|
||||
cg_snapshot = self.tab_group.kwargs['cg_snapshot']
|
||||
return {"cg_snapshot": cg_snapshot}
|
||||
|
||||
def get_redirect_url(self):
|
||||
return reverse('horizon:project:volumes:cg_snapshots:index')
|
||||
|
||||
|
||||
class CGSnapshotsDetailTabs(tabs.TabGroup):
|
||||
slug = "cg_snapshots_details"
|
||||
tabs = (OverviewTab,)
|
@ -0,0 +1,166 @@
|
||||
# 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 django.utils.http import urlunquote
|
||||
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_CG_SNAPSHOTS_TAB_URL = urlunquote(reverse(
|
||||
'horizon:project:volumes:cg_snapshots_tab'))
|
||||
|
||||
|
||||
class CGroupSnapshotTests(test.TestCase):
|
||||
@test.create_stubs({cinder: ('volume_cg_snapshot_get',
|
||||
'volume_cgroup_create_from_source',)})
|
||||
def test_create_cgroup_from_snapshot(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
cg_snapshot = self.cinder_cg_snapshots.first()
|
||||
formData = {'cg_snapshot_id': cg_snapshot.id,
|
||||
'name': 'test CG SS Create',
|
||||
'description': 'test desc'}
|
||||
|
||||
cinder.volume_cg_snapshot_get(IsA(http.HttpRequest), cg_snapshot.id).\
|
||||
AndReturn(cg_snapshot)
|
||||
cinder.volume_cgroup_create_from_source(
|
||||
IsA(http.HttpRequest),
|
||||
formData['name'],
|
||||
source_cgroup_id=formData['cg_snapshot_id'],
|
||||
description=formData['description'])\
|
||||
.AndReturn(cgroup)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cg_snapshots:create_cgroup',
|
||||
args=[cg_snapshot.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cg_snapshot_get',
|
||||
'volume_cgroup_create_from_source',)})
|
||||
def test_create_cgroup_from_snapshot_exception(self):
|
||||
cg_snapshot = self.cinder_cg_snapshots.first()
|
||||
new_cg_name = 'test CG SS Create'
|
||||
formData = {'cg_snapshot_id': cg_snapshot.id,
|
||||
'name': new_cg_name,
|
||||
'description': 'test desc'}
|
||||
|
||||
cinder.volume_cg_snapshot_get(IsA(http.HttpRequest), cg_snapshot.id).\
|
||||
AndReturn(cg_snapshot)
|
||||
cinder.volume_cgroup_create_from_source(
|
||||
IsA(http.HttpRequest),
|
||||
formData['name'],
|
||||
source_cgroup_id=formData['cg_snapshot_id'],
|
||||
description=formData['description'])\
|
||||
.AndRaise(self.exceptions.cinder)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cg_snapshots:create_cgroup',
|
||||
args=[cg_snapshot.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
# There are a bunch of backslashes for formatting in the message from
|
||||
# the response, so remove them when validating the error message.
|
||||
self.assertIn('Unable to create consistency group "%s" from snapshot.'
|
||||
% new_cg_name,
|
||||
res.cookies.output().replace('\\', ''))
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cg_snapshot_list',
|
||||
'volume_cg_snapshot_delete',)})
|
||||
def test_delete_cgroup_snapshot(self):
|
||||
cg_snapshots = self.cinder_cg_snapshots.list()
|
||||
cg_snapshot = self.cinder_cg_snapshots.first()
|
||||
|
||||
cinder.volume_cg_snapshot_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(cg_snapshots)
|
||||
cinder.volume_cg_snapshot_delete(IsA(http.HttpRequest), cg_snapshot.id)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
form_data = {'action': 'volume_cg_snapshots__delete_cg_snapshot__%s'
|
||||
% cg_snapshot.id}
|
||||
res = self.client.post(VOLUME_CG_SNAPSHOTS_TAB_URL, form_data,
|
||||
follow=True)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertIn("Scheduled deletion of Snapshot: %s" % cg_snapshot.name,
|
||||
[m.message for m in res.context['messages']])
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cg_snapshot_list',
|
||||
'volume_cg_snapshot_delete',)})
|
||||
def test_delete_cgroup_snapshot_exception(self):
|
||||
cg_snapshots = self.cinder_cg_snapshots.list()
|
||||
cg_snapshot = self.cinder_cg_snapshots.first()
|
||||
|
||||
cinder.volume_cg_snapshot_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(cg_snapshots)
|
||||
cinder.volume_cg_snapshot_delete(IsA(http.HttpRequest),
|
||||
cg_snapshot.id).\
|
||||
AndRaise(self.exceptions.cinder)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
form_data = {'action': 'volume_cg_snapshots__delete_cg_snapshot__%s'
|
||||
% cg_snapshot.id}
|
||||
res = self.client.post(VOLUME_CG_SNAPSHOTS_TAB_URL, form_data,
|
||||
follow=True)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertIn("Unable to delete snapshot: %s" % cg_snapshot.name,
|
||||
[m.message for m in res.context['messages']])
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cg_snapshot_get',
|
||||
'volume_cgroup_get',
|
||||
'volume_type_get',
|
||||
'volume_list',)})
|
||||
def test_detail_view(self):
|
||||
cg_snapshot = self.cinder_cg_snapshots.first()
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
volume_type = self.cinder_volume_types.first()
|
||||
volumes = self.cinder_volumes.list()
|
||||
|
||||
cinder.volume_cg_snapshot_get(IsA(http.HttpRequest), cg_snapshot.id).\
|
||||
AndReturn(cg_snapshot)
|
||||
cinder.volume_cgroup_get(IsA(http.HttpRequest), cgroup.id).\
|
||||
AndReturn(cgroup)
|
||||
cinder.volume_type_get(IsA(http.HttpRequest), volume_type.id).\
|
||||
MultipleTimes().AndReturn(volume_type)
|
||||
search_opts = {'consistencygroup_id': cgroup.id}
|
||||
cinder.volume_list(IsA(http.HttpRequest), search_opts=search_opts).\
|
||||
AndReturn(volumes)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse(
|
||||
'horizon:project:volumes:cg_snapshots:cg_snapshot_detail',
|
||||
args=[cg_snapshot.id])
|
||||
res = self.client.get(url)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cg_snapshot_get',)})
|
||||
def test_detail_view_with_exception(self):
|
||||
cg_snapshot = self.cinder_cg_snapshots.first()
|
||||
|
||||
cinder.volume_cg_snapshot_get(IsA(http.HttpRequest), cg_snapshot.id).\
|
||||
AndRaise(self.exceptions.cinder)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse(
|
||||
'horizon:project:volumes:cg_snapshots:cg_snapshot_detail',
|
||||
args=[cg_snapshot.id])
|
||||
res = self.client.get(url)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
@ -0,0 +1,26 @@
|
||||
# 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.cg_snapshots import views
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^(?P<cg_snapshot_id>[^/]+)/cg_snapshot_detail/$',
|
||||
views.DetailView.as_view(),
|
||||
name='cg_snapshot_detail'),
|
||||
url(r'^(?P<cg_snapshot_id>[^/]+)/create_cgroup/$',
|
||||
views.CreateCGroupView.as_view(),
|
||||
name='create_cgroup'),
|
||||
)
|
@ -0,0 +1,138 @@
|
||||
# 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 openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.usage import quotas
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.cg_snapshots import forms as cg_snapshot_forms
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.cg_snapshots import tables as cg_snapshot_tables
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.cg_snapshots import tabs as cg_snapshot_tabs
|
||||
|
||||
CGROUP_INFO_FIELDS = ("name",
|
||||
"description")
|
||||
|
||||
INDEX_URL = "horizon:project:volumes:index"
|
||||
|
||||
|
||||
class DetailView(tabs.TabView):
|
||||
tab_group_class = cg_snapshot_tabs.CGSnapshotsDetailTabs
|
||||
template_name = 'horizon/common/_detail.html'
|
||||
page_title = "{{ cg_snapshot.name|default:cg_snapshot.id }}"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
cg_snapshot = self.get_data()
|
||||
table = cg_snapshot_tables.CGSnapshotsTable(self.request)
|
||||
context["cg_snapshot"] = cg_snapshot
|
||||
context["url"] = self.get_redirect_url()
|
||||
context["actions"] = table.render_row_actions(cg_snapshot)
|
||||
return context
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self):
|
||||
try:
|
||||
cg_snapshot_id = self.kwargs['cg_snapshot_id']
|
||||
cg_snapshot = api.cinder.volume_cg_snapshot_get(self.request,
|
||||
cg_snapshot_id)
|
||||
|
||||
cgroup_id = cg_snapshot.consistencygroup_id
|
||||
cgroup = api.cinder.volume_cgroup_get(self.request,
|
||||
cgroup_id)
|
||||
cg_snapshot.cg_name = cgroup.name
|
||||
cg_snapshot.volume_type_names = []
|
||||
for vol_type_id in cgroup.volume_types:
|
||||
vol_type = api.cinder.volume_type_get(self.request,
|
||||
vol_type_id)
|
||||
cg_snapshot.volume_type_names.append(vol_type.name)
|
||||
|
||||
cg_snapshot.volume_names = []
|
||||
search_opts = {'consistencygroup_id': cgroup_id}
|
||||
volumes = api.cinder.volume_list(self.request,
|
||||
search_opts=search_opts)
|
||||
for volume in volumes:
|
||||
cg_snapshot.volume_names.append(volume.name)
|
||||
|
||||
except Exception:
|
||||
redirect = self.get_redirect_url()
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve consistency group '
|
||||
'snapshot details.'),
|
||||
redirect=redirect)
|
||||
return cg_snapshot
|
||||
|
||||
@staticmethod
|
||||
def get_redirect_url():
|
||||
return reverse('horizon:project:volumes:index')
|
||||
|
||||
def get_tabs(self, request, *args, **kwargs):
|
||||
cg_snapshot = self.get_data()
|
||||
return self.tab_group_class(request, cg_snapshot=cg_snapshot, **kwargs)
|
||||
|
||||
|
||||
class CreateCGroupView(forms.ModalFormView):
|
||||
form_class = cg_snapshot_forms.CreateCGroupForm
|
||||
modal_header = _("Create Consistency Group")
|
||||
template_name = 'project/volumes/cg_snapshots/create.html'
|
||||
submit_url = "horizon:project:volumes:cg_snapshots:create_cgroup"
|
||||
success_url = reverse_lazy('horizon:project:volumes:cgroups_tab')
|
||||
page_title = _("Create a Volume Consistency Group from Snapshot")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CreateCGroupView, self).get_context_data(**kwargs)
|
||||
context['cg_snapshot_id'] = self.kwargs['cg_snapshot_id']
|
||||
args = (self.kwargs['cg_snapshot_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
try:
|
||||
# get number of volumes we will be creating
|
||||
cg_snapshot = cinder.volume_cg_snapshot_get(
|
||||
self.request, context['cg_snapshot_id'])
|
||||
|
||||
cgroup_id = cg_snapshot.consistencygroup_id
|
||||
|
||||
search_opts = {'consistencygroup_id': cgroup_id}
|
||||
volumes = api.cinder.volume_list(self.request,
|
||||
search_opts=search_opts)
|
||||
num_volumes = len(volumes)
|
||||
usages = quotas.tenant_limit_usages(self.request)
|
||||
|
||||
if usages['volumesUsed'] + num_volumes > \
|
||||
usages['maxTotalVolumes']:
|
||||
raise ValueError(_('Unable to create consistency group due to '
|
||||
'exceeding volume quota limit.'))
|
||||
else:
|
||||
usages['numRequestedItems'] = num_volumes
|
||||
context['usages'] = usages
|
||||
|
||||
except ValueError as e:
|
||||
exceptions.handle(self.request, e.message)
|
||||
return None
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve consistency '
|
||||
'group information.'))
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
return {'cg_snapshot_id': self.kwargs["cg_snapshot_id"]}
|
@ -28,8 +28,21 @@ class UpdateForm(forms.SelfHandlingForm):
|
||||
label=_("Description"),
|
||||
required=False)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(UpdateForm, self).clean()
|
||||
new_desc = cleaned_data.get('description')
|
||||
old_desc = self.initial['description']
|
||||
if len(old_desc):
|
||||
if len(new_desc) == 0:
|
||||
error_msg = _("Description is required.")
|
||||
self._errors['description'] = self.error_class([error_msg])
|
||||
return cleaned_data
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def handle(self, request, data):
|
||||
cgroup_id = self.initial['cgroup_id']
|
||||
|
||||
try:
|
||||
cinder.volume_cgroup_update(request,
|
||||
cgroup_id,
|
||||
@ -45,3 +58,160 @@ class UpdateForm(forms.SelfHandlingForm):
|
||||
exceptions.handle(request,
|
||||
_('Unable to update volume consistency group.'),
|
||||
redirect=redirect)
|
||||
|
||||
|
||||
class RemoveVolsForm(forms.SelfHandlingForm):
|
||||
def handle(self, request, data):
|
||||
cgroup_id = self.initial['cgroup_id']
|
||||
name = self.initial['name']
|
||||
search_opts = {'consistencygroup_id': cgroup_id}
|
||||
|
||||
try:
|
||||
# get list of assigned volumes
|
||||
assigned_vols = []
|
||||
volumes = cinder.volume_list(request,
|
||||
search_opts=search_opts)
|
||||
for volume in volumes:
|
||||
assigned_vols.append(volume.id)
|
||||
|
||||
assigned_vols_str = ",".join(assigned_vols)
|
||||
cinder.volume_cgroup_update(request,
|
||||
cgroup_id,
|
||||
remove_vols=assigned_vols_str)
|
||||
|
||||
message = _('Removing volumes from volume consistency '
|
||||
'group "%s"') % name
|
||||
messages.info(request, message)
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
redirect = reverse("horizon:project:volumes:index")
|
||||
exceptions.handle(request, _('Errors occurred in removing volumes '
|
||||
'from consistency group.'),
|
||||
redirect=redirect)
|
||||
|
||||
|
||||
class DeleteForm(forms.SelfHandlingForm):
|
||||
delete_volumes = forms.BooleanField(label=_("Delete Volumes"),
|
||||
required=False)
|
||||
|
||||
def handle(self, request, data):
|
||||
cgroup_id = self.initial['cgroup_id']
|
||||
name = self.initial['name']
|
||||
delete_volumes = data['delete_volumes']
|
||||
|
||||
try:
|
||||
cinder.volume_cgroup_delete(request,
|
||||
cgroup_id,
|
||||
force=delete_volumes)
|
||||
message = _('Deleting volume consistency '
|
||||
'group "%s"') % name
|
||||
messages.success(request, message)
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
redirect = reverse("horizon:project:volumes:index")
|
||||
exceptions.handle(request, _('Errors occurred in deleting '
|
||||
'consistency group.'),
|
||||
redirect=redirect)
|
||||
|
||||
|
||||
class CreateSnapshotForm(forms.SelfHandlingForm):
|
||||
name = forms.CharField(max_length=255, label=_("Snapshot Name"))
|
||||
description = forms.CharField(max_length=255,
|
||||
widget=forms.Textarea(attrs={'rows': 4}),
|
||||
label=_("Description"),
|
||||
required=False)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(CreateSnapshotForm, self).__init__(request, *args, **kwargs)
|
||||
|
||||
# populate cgroup_id
|
||||
cgroup_id = kwargs.get('initial', {}).get('cgroup_id', [])
|
||||
self.fields['cgroup_id'] = forms.CharField(widget=forms.HiddenInput(),
|
||||
initial=cgroup_id)
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
message = _('Creating consistency group snapshot "%s".') \
|
||||
% data['name']
|
||||
snapshot = cinder.volume_cg_snapshot_create(request,
|
||||
data['cgroup_id'],
|
||||
data['name'],
|
||||
data['description'])
|
||||
|
||||
messages.info(request, message)
|
||||
return snapshot
|
||||
except Exception as e:
|
||||
redirect = reverse("horizon:project:volumes:index")
|
||||
msg = _('Unable to create consistency group snapshot.')
|
||||
if e.code == 413:
|
||||
msg = _('Requested snapshot would exceed the allowed quota.')
|
||||
else:
|
||||
search_opts = {'consistentcygroup_id': data['cgroup_id']}
|
||||
volumes = cinder.volume_list(request, search_opts=search_opts)
|
||||
if len(volumes) == 0:
|
||||
msg = _('Unable to create snapshot. Consistency group '
|
||||
'must contain volumes.')
|
||||
|
||||
exceptions.handle(request,
|
||||
msg,
|
||||
redirect=redirect)
|
||||
|
||||
|
||||
class CloneCGroupForm(forms.SelfHandlingForm):
|
||||
name = forms.CharField(max_length=255, label=_("Consistency Group Name"))
|
||||
description = forms.CharField(max_length=255,
|
||||
widget=forms.Textarea(attrs={'rows': 4}),
|
||||
label=_("Description"),
|
||||
required=False)
|
||||
cgroup_source = forms.ChoiceField(
|
||||
label=_("Use a consistency group as source"),
|
||||
widget=forms.SelectWidget(
|
||||
attrs={'class': 'image-selector'},
|
||||
data_attrs=('name'),
|
||||
transform=lambda x: "%s" % (x.name)),
|
||||
required=False)
|
||||
|
||||
def prepare_cgroup_source_field(self, request, cgroup_id):
|
||||
try:
|
||||
cgroup = cinder.volume_cgroup_get(request,
|
||||
cgroup_id)
|
||||
self.fields['cgroup_source'].choices = ((cgroup_id,
|
||||
cgroup),)
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Unable to load the specified '
|
||||
'consistency group.'))
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(CloneCGroupForm, self).__init__(request, *args, **kwargs)
|
||||
|
||||
# populate cgroup_id
|
||||
cgroup_id = kwargs.get('initial', {}).get('cgroup_id', [])
|
||||
self.fields['cgroup_id'] = forms.CharField(widget=forms.HiddenInput(),
|
||||
initial=cgroup_id)
|
||||
self.prepare_cgroup_source_field(request, cgroup_id)
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
message = _('Creating consistency group "%s".') % data['name']
|
||||
cgroup = cinder.volume_cgroup_create_from_source(
|
||||
request,
|
||||
data['name'],
|
||||
source_cgroup_id=data['cgroup_id'],
|
||||
description=data['description'])
|
||||
|
||||
messages.info(request, message)
|
||||
return cgroup
|
||||
except Exception:
|
||||
redirect = reverse("horizon:project:volumes:index")
|
||||
msg = _('Unable to clone consistency group.')
|
||||
|
||||
search_opts = {'consistentcygroup_id': data['cgroup_id']}
|
||||
volumes = cinder.volume_list(request, search_opts=search_opts)
|
||||
if len(volumes) == 0:
|
||||
msg = _('Unable to clone empty consistency group.')
|
||||
|
||||
exceptions.handle(request,
|
||||
msg,
|
||||
redirect=redirect)
|
||||
|
@ -10,10 +10,8 @@
|
||||
# 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
|
||||
@ -31,36 +29,20 @@ class CreateVolumeCGroup(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
policy_rules = (("volume", "consistencygroup:create"),)
|
||||
|
||||
|
||||
class DeleteVolumeCGroup(policy.PolicyTargetMixin, tables.DeleteAction):
|
||||
class DeleteVolumeCGroup(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
name = "deletecg"
|
||||
verbose_name = _("Delete Consistency Group")
|
||||
url = "horizon:project:volumes:cgroups:delete"
|
||||
classes = ("ajax-modal", "btn-danger")
|
||||
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 RemoveAllVolumes(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
name = "remove_vols"
|
||||
verbose_name = _("Remove Volumes from Consistency Group")
|
||||
url = "horizon:project:volumes:cgroups:remove_volumes"
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "consistencygroup:update"), )
|
||||
|
||||
|
||||
class EditVolumeCGroup(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
@ -78,12 +60,51 @@ class ManageVolumes(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "consistencygroup:update"),)
|
||||
|
||||
def allowed(self, request, cgroup=None):
|
||||
if hasattr(cgroup, 'status'):
|
||||
return cgroup.status != 'error'
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class CreateSnapshot(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
name = "create_snapshot"
|
||||
verbose_name = _("Create Snapshot")
|
||||
url = "horizon:project:volumes:cgroups:create_snapshot"
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "consistencygroup:create_cgsnapshot"),)
|
||||
|
||||
def allowed(self, request, cgroup=None):
|
||||
if hasattr(cgroup, 'status'):
|
||||
return cgroup.status != 'error'
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class CloneCGroup(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
name = "clone_cgroup"
|
||||
verbose_name = _("Clone Consistency Group")
|
||||
url = "horizon:project:volumes:cgroups:clone_cgroup"
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "consistencygroup:create"),)
|
||||
|
||||
def allowed(self, request, cgroup=None):
|
||||
if hasattr(cgroup, 'status'):
|
||||
return cgroup.status != 'error'
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class UpdateRow(tables.Row):
|
||||
ajax = True
|
||||
|
||||
def get_data(self, request, cgroup_id):
|
||||
cgroup = cinder.volume_cgroup_get(request, cgroup_id)
|
||||
try:
|
||||
cgroup = cinder.volume_cgroup_get_with_vol_type_names(request,
|
||||
cgroup_id)
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Unable to display '
|
||||
'consistency group.'))
|
||||
return cgroup
|
||||
|
||||
|
||||
@ -97,6 +118,8 @@ class VolumeCGroupsFilterAction(tables.FilterAction):
|
||||
|
||||
|
||||
def get_volume_types(cgroup):
|
||||
vtypes_str = ''
|
||||
if hasattr(cgroup, 'volume_type_names'):
|
||||
vtypes_str = ",".join(cgroup.volume_type_names)
|
||||
return vtypes_str
|
||||
|
||||
@ -143,6 +166,9 @@ class VolumeCGroupsTable(tables.DataTable):
|
||||
VolumeCGroupsFilterAction)
|
||||
row_actions = (ManageVolumes,
|
||||
EditVolumeCGroup,
|
||||
CreateSnapshot,
|
||||
CloneCGroup,
|
||||
RemoveAllVolumes,
|
||||
DeleteVolumeCGroup)
|
||||
row_class = UpdateRow
|
||||
status_columns = ("status",)
|
||||
|
@ -10,7 +10,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import django
|
||||
from django.core.urlresolvers import reverse
|
||||
from django import http
|
||||
from django.utils.http import urlunquote
|
||||
@ -23,34 +22,36 @@ from openstack_dashboard.test import helpers as test
|
||||
VOLUME_INDEX_URL = reverse('horizon:project:volumes:index')
|
||||
VOLUME_CGROUPS_TAB_URL = urlunquote(reverse(
|
||||
'horizon:project:volumes:cgroups_tab'))
|
||||
VOLUME_CGROUPS_SNAP_TAB_URL = urlunquote(reverse(
|
||||
'horizon:project:volumes:cg_snapshots_tab'))
|
||||
|
||||
|
||||
class ConsistencyGroupTests(test.TestCase):
|
||||
@test.create_stubs({cinder: ('volume_cgroup_create',
|
||||
'volume_cgroup_list',
|
||||
@test.create_stubs({cinder: ('extension_supported',
|
||||
'availability_zone_list',
|
||||
'volume_type_list',
|
||||
'volume_type_list_with_qos_associations',
|
||||
'availability_zone_list',
|
||||
'extension_supported')})
|
||||
'volume_cgroup_list',
|
||||
'volume_cgroup_create')})
|
||||
def test_create_cgroup(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
volume_types = self.cinder_volume_types.list()
|
||||
volume_type_id = self.cinder_volume_types.first().id
|
||||
az = self.cinder_availability_zones.first().zoneName
|
||||
formData = {'volume_types': '1',
|
||||
'name': 'test CG',
|
||||
'description': 'test desc',
|
||||
'availability_zone': az}
|
||||
'availability_zone': az,
|
||||
'add_vtypes_to_cgroup_role_member': [volume_type_id]}
|
||||
|
||||
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)).\
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).AndReturn(volume_types)
|
||||
cinder.volume_type_list_with_qos_associations(IsA(http.HttpRequest)).\
|
||||
AndReturn(volume_types)
|
||||
cinder.volume_cgroup_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.cinder_consistencygroups.list())
|
||||
cinder.volume_cgroup_create(
|
||||
IsA(http.HttpRequest),
|
||||
@ -64,31 +65,32 @@ class ConsistencyGroupTests(test.TestCase):
|
||||
url = reverse('horizon:project:volumes:cgroups:create')
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_create',
|
||||
'volume_cgroup_list',
|
||||
@test.create_stubs({cinder: ('extension_supported',
|
||||
'availability_zone_list',
|
||||
'volume_type_list',
|
||||
'volume_type_list_with_qos_associations',
|
||||
'availability_zone_list',
|
||||
'extension_supported')})
|
||||
'volume_cgroup_list',
|
||||
'volume_cgroup_create')})
|
||||
def test_create_cgroup_exception(self):
|
||||
volume_types = self.cinder_volume_types.list()
|
||||
volume_type_id = self.cinder_volume_types.first().id
|
||||
az = self.cinder_availability_zones.first().zoneName
|
||||
formData = {'volume_types': '1',
|
||||
'name': 'test CG',
|
||||
'description': 'test desc',
|
||||
'availability_zone': az}
|
||||
'availability_zone': az,
|
||||
'add_vtypes_to_cgroup_role_member': [volume_type_id]}
|
||||
|
||||
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)).\
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).AndReturn(volume_types)
|
||||
cinder.volume_type_list_with_qos_associations(IsA(http.HttpRequest)).\
|
||||
AndReturn(volume_types)
|
||||
cinder.volume_cgroup_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.cinder_consistencygroups.list())
|
||||
cinder.volume_cgroup_create(
|
||||
IsA(http.HttpRequest),
|
||||
@ -101,29 +103,65 @@ class ConsistencyGroupTests(test.TestCase):
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:create')
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
self.assertIn("Unable to create consistency group.",
|
||||
res.cookies.output())
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_list_with_vol_type_names',
|
||||
@test.create_stubs({cinder: ('volume_cgroup_get',
|
||||
'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_get(IsA(http.HttpRequest), cgroup.id).\
|
||||
AndReturn(cgroup)
|
||||
cinder.volume_cgroup_delete(IsA(http.HttpRequest), cgroup.id,
|
||||
force=False)
|
||||
if django.VERSION < (1, 9):
|
||||
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']])
|
||||
url = reverse('horizon:project:volumes:cgroups:delete',
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_get',
|
||||
'volume_cgroup_delete')})
|
||||
def test_delete_cgroup_force_flag(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
formData = {'delete_volumes': True}
|
||||
|
||||
cinder.volume_cgroup_get(IsA(http.HttpRequest), cgroup.id).\
|
||||
AndReturn(cgroup)
|
||||
cinder.volume_cgroup_delete(IsA(http.HttpRequest), cgroup.id,
|
||||
force=True)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:delete',
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_get',
|
||||
'volume_cgroup_delete')})
|
||||
def test_delete_cgroup_exception(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
formData = {'delete_volumes': False}
|
||||
|
||||
cinder.volume_cgroup_get(IsA(http.HttpRequest), cgroup.id).\
|
||||
AndReturn(cgroup)
|
||||
cinder.volume_cgroup_delete(IsA(http.HttpRequest),
|
||||
cgroup.id,
|
||||
force=False).\
|
||||
AndRaise(self.exceptions.cinder)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:delete',
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_update',
|
||||
'volume_cgroup_get')})
|
||||
@ -149,6 +187,7 @@ class ConsistencyGroupTests(test.TestCase):
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_update',
|
||||
'volume_cgroup_get')})
|
||||
@ -174,6 +213,7 @@ class ConsistencyGroupTests(test.TestCase):
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_update',
|
||||
'volume_cgroup_get')})
|
||||
@ -197,6 +237,7 @@ class ConsistencyGroupTests(test.TestCase):
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_update',
|
||||
'volume_cgroup_get')})
|
||||
@ -219,7 +260,7 @@ class ConsistencyGroupTests(test.TestCase):
|
||||
url = reverse('horizon:project:volumes:cgroups:update',
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_get',)})
|
||||
@ -234,5 +275,48 @@ class ConsistencyGroupTests(test.TestCase):
|
||||
url = reverse('horizon:project:volumes:cgroups:detail',
|
||||
args=[cgroup.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cg_snapshot_create',)})
|
||||
def test_create_snapshot(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
cg_snapshot = self.cinder_cg_snapshots.first()
|
||||
formData = {'cgroup_id': cgroup.id,
|
||||
'name': 'test CG Snapshot',
|
||||
'description': 'test desc'}
|
||||
|
||||
cinder.volume_cg_snapshot_create(
|
||||
IsA(http.HttpRequest),
|
||||
formData['cgroup_id'],
|
||||
formData['name'],
|
||||
formData['description'])\
|
||||
.AndReturn(cg_snapshot)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:create_snapshot',
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_CGROUPS_SNAP_TAB_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_cgroup_get',
|
||||
'volume_cgroup_create_from_source',)})
|
||||
def test_create_clone(self):
|
||||
cgroup = self.cinder_consistencygroups.first()
|
||||
formData = {'cgroup_id': cgroup.id,
|
||||
'name': 'test CG Clone',
|
||||
'description': 'test desc'}
|
||||
cinder.volume_cgroup_create_from_source(
|
||||
IsA(http.HttpRequest),
|
||||
formData['name'],
|
||||
source_cgroup_id=formData['cgroup_id'],
|
||||
description=formData['description'])\
|
||||
.AndReturn(cgroup)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:cgroups:clone_cgroup',
|
||||
args=[cgroup.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_CGROUPS_TAB_URL)
|
||||
|
@ -22,9 +22,21 @@ urlpatterns = [
|
||||
url(r'^(?P<cgroup_id>[^/]+)/update/$',
|
||||
views.UpdateView.as_view(),
|
||||
name='update'),
|
||||
url(r'^(?P<cgroup_id>[^/]+)/remove_volumese/$',
|
||||
views.RemoveVolumesView.as_view(),
|
||||
name='remove_volumes'),
|
||||
url(r'^(?P<cgroup_id>[^/]+)/delete/$',
|
||||
views.DeleteView.as_view(),
|
||||
name='delete'),
|
||||
url(r'^(?P<cgroup_id>[^/]+)/manage/$',
|
||||
views.ManageView.as_view(),
|
||||
name='manage'),
|
||||
url(r'^(?P<cgroup_id>[^/]+)/create_snapshot/$',
|
||||
views.CreateSnapshotView.as_view(),
|
||||
name='create_snapshot'),
|
||||
url(r'^(?P<cgroup_id>[^/]+)/clone_cgroup/$',
|
||||
views.CloneCGroupView.as_view(),
|
||||
name='clone_cgroup'),
|
||||
url(r'^(?P<cgroup_id>[^/]+)$',
|
||||
views.DetailView.as_view(),
|
||||
name='detail'),
|
||||
|
@ -22,6 +22,7 @@ from horizon import workflows
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.usage import quotas
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.cgroups import workflows as vol_cgroup_workflows
|
||||
@ -50,7 +51,7 @@ class UpdateView(forms.ModalFormView):
|
||||
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
|
||||
submit_label = _("Submit")
|
||||
page_title = modal_header
|
||||
|
||||
def get_initial(self):
|
||||
@ -78,6 +79,72 @@ class UpdateView(forms.ModalFormView):
|
||||
return self._object
|
||||
|
||||
|
||||
class RemoveVolumesView(forms.ModalFormView):
|
||||
template_name = 'project/volumes/cgroups/remove_vols.html'
|
||||
modal_header = _("Remove Volumes from Consistency Group")
|
||||
form_class = vol_cgroup_forms.RemoveVolsForm
|
||||
success_url = reverse_lazy('horizon:project:volumes:index')
|
||||
submit_url = "horizon:project:volumes:cgroups:remove_volumes"
|
||||
submit_label = _("Submit")
|
||||
page_title = modal_header
|
||||
|
||||
def get_initial(self):
|
||||
cgroup = self.get_object()
|
||||
return {'cgroup_id': self.kwargs["cgroup_id"],
|
||||
'name': cgroup.name}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RemoveVolumesView, 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 DeleteView(forms.ModalFormView):
|
||||
template_name = 'project/volumes/cgroups/delete.html'
|
||||
modal_header = _("Delete Consistency Group")
|
||||
form_class = vol_cgroup_forms.DeleteForm
|
||||
success_url = reverse_lazy('horizon:project:volumes:index')
|
||||
submit_url = "horizon:project:volumes:cgroups:delete"
|
||||
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}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DeleteView, 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
|
||||
|
||||
@ -105,6 +172,94 @@ class ManageView(workflows.WorkflowView):
|
||||
'vtypes': getattr(cgroup, "volume_types")}
|
||||
|
||||
|
||||
class CreateSnapshotView(forms.ModalFormView):
|
||||
form_class = vol_cgroup_forms.CreateSnapshotForm
|
||||
modal_header = _("Create Consistency Group Snapshot")
|
||||
template_name = 'project/volumes/cgroups/create_snapshot.html'
|
||||
submit_label = _("Create Snapshot")
|
||||
submit_url = "horizon:project:volumes:cgroups:create_snapshot"
|
||||
success_url = reverse_lazy('horizon:project:volumes:cg_snapshots_tab')
|
||||
page_title = modal_header
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CreateSnapshotView, 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)
|
||||
try:
|
||||
# get number of snapshots we will be creating
|
||||
search_opts = {'consistencygroup_id': context['cgroup_id']}
|
||||
volumes = api.cinder.volume_list(self.request,
|
||||
search_opts=search_opts)
|
||||
num_volumes = len(volumes)
|
||||
usages = quotas.tenant_limit_usages(self.request)
|
||||
|
||||
if usages['snapshotsUsed'] + num_volumes > \
|
||||
usages['maxTotalSnapshots']:
|
||||
raise ValueError(_('Unable to create snapshots due to '
|
||||
'exceeding snapshot quota limit.'))
|
||||
else:
|
||||
usages['numRequestedItems'] = num_volumes
|
||||
context['usages'] = usages
|
||||
|
||||
except ValueError as e:
|
||||
exceptions.handle(self.request, e.message)
|
||||
return None
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve consistency '
|
||||
'group information.'))
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
return {'cgroup_id': self.kwargs["cgroup_id"]}
|
||||
|
||||
|
||||
class CloneCGroupView(forms.ModalFormView):
|
||||
form_class = vol_cgroup_forms.CloneCGroupForm
|
||||
modal_header = _("Clone Consistency Group")
|
||||
template_name = 'project/volumes/cgroups/clone_cgroup.html'
|
||||
submit_label = _("Clone Consistency Group")
|
||||
submit_url = "horizon:project:volumes:cgroups:clone_cgroup"
|
||||
success_url = reverse_lazy('horizon:project:volumes:cgroups_tab')
|
||||
page_title = modal_header
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CloneCGroupView, 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)
|
||||
try:
|
||||
# get number of volumes we will be creating
|
||||
cgroup_id = context['cgroup_id']
|
||||
|
||||
search_opts = {'consistencygroup_id': cgroup_id}
|
||||
volumes = api.cinder.volume_list(self.request,
|
||||
search_opts=search_opts)
|
||||
num_volumes = len(volumes)
|
||||
usages = quotas.tenant_limit_usages(self.request)
|
||||
|
||||
if usages['volumesUsed'] + num_volumes > \
|
||||
usages['maxTotalVolumes']:
|
||||
raise ValueError(_('Unable to create consistency group due to '
|
||||
'exceeding volume quota limit.'))
|
||||
else:
|
||||
usages['numRequestedItems'] = num_volumes
|
||||
context['usages'] = usages
|
||||
|
||||
except ValueError as e:
|
||||
exceptions.handle(self.request, e.message)
|
||||
return None
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve consistency '
|
||||
'group information.'))
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
return {'cgroup_id': self.kwargs["cgroup_id"]}
|
||||
|
||||
|
||||
class DetailView(tabs.TabView):
|
||||
tab_group_class = vol_cgroup_tabs.CGroupsDetailTabs
|
||||
template_name = 'horizon/common/_detail.html'
|
||||
|
@ -15,7 +15,6 @@ 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
|
||||
@ -100,10 +99,14 @@ class AddCGroupInfoAction(workflows.Action):
|
||||
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.')
|
||||
% name
|
||||
% formatted_name
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
@ -136,19 +139,25 @@ class AddVolumeTypesToCGroupAction(workflows.MembershipAction):
|
||||
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]
|
||||
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"
|
||||
|
||||
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
|
||||
|
||||
|
||||
class AddVolTypesToCGroupStep(workflows.UpdateMembersStep):
|
||||
action_class = AddVolumeTypesToCGroupAction
|
||||
@ -198,15 +207,28 @@ class AddVolumesToCGroupAction(workflows.MembershipAction):
|
||||
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'):
|
||||
if volume.consistencygroup_id == \
|
||||
self.initial['cgroup_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
|
||||
@ -228,9 +250,9 @@ class AddVolumesToCGroupAction(workflows.MembershipAction):
|
||||
" [" + volume['volume_id'] + "]"
|
||||
else:
|
||||
entry = volume['volume_name']
|
||||
available_vols.append((entry, entry))
|
||||
available_vols.append((volume['volume_id'], entry))
|
||||
if volume['in_cgroup']:
|
||||
assigned_vols.append(entry)
|
||||
assigned_vols.append(volume['volume_id'])
|
||||
|
||||
except Exception:
|
||||
exceptions.handle(request, err_msg)
|
||||
@ -291,7 +313,7 @@ class CreateCGroupWorkflow(workflows.Workflow):
|
||||
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 selected_vol_type == vol_type.id:
|
||||
if hasattr(vol_type, "extra_specs"):
|
||||
vol_type_backend = \
|
||||
vol_type.extra_specs['volume_backend_name']
|
||||
@ -319,7 +341,7 @@ class CreateCGroupWorkflow(workflows.Workflow):
|
||||
cinder.volume_cgroup_create(
|
||||
request,
|
||||
vtypes_str,
|
||||
name=context['name'],
|
||||
context['name'],
|
||||
description=context['description'],
|
||||
availability_zone=context['availability_zone'])
|
||||
except Exception:
|
||||
@ -331,11 +353,11 @@ class CreateCGroupWorkflow(workflows.Workflow):
|
||||
|
||||
|
||||
class UpdateCGroupWorkflow(workflows.Workflow):
|
||||
slug = "create_cgroup"
|
||||
slug = "update_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')
|
||||
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,)
|
||||
|
||||
@ -351,23 +373,8 @@ class UpdateCGroupWorkflow(workflows.Workflow):
|
||||
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:
|
||||
if selection == volume.id:
|
||||
selected = True
|
||||
else:
|
||||
selected = True
|
||||
|
||||
if selected:
|
||||
break
|
||||
|
||||
if selected:
|
||||
@ -381,43 +388,24 @@ class UpdateCGroupWorkflow(workflows.Workflow):
|
||||
# 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)
|
||||
|
||||
# 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.'))
|
||||
# error message supplied by form
|
||||
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
|
||||
|
@ -24,8 +24,10 @@ from openstack_dashboard import policy
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes.backups \
|
||||
import tables as backups_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.cg_snapshots \
|
||||
import tables as cg_snapshots_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.cgroups \
|
||||
import tables as vol_cgroup_tables
|
||||
import tables as cgroup_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.snapshots \
|
||||
import tables as vol_snapshot_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.volumes \
|
||||
@ -206,9 +208,9 @@ class BackupsTab(PagedTableMixin, tabs.TableTab, VolumeTableMixIn):
|
||||
return backups
|
||||
|
||||
|
||||
class CGroupsTab(tabs.TableTab, VolumeTableMixIn):
|
||||
table_classes = (vol_cgroup_tables.VolumeCGroupsTable,)
|
||||
name = _("Volume Consistency Groups")
|
||||
class CGroupsTab(tabs.TableTab):
|
||||
table_classes = (cgroup_tables.VolumeCGroupsTable,)
|
||||
name = _("Consistency Groups")
|
||||
slug = "cgroups_tab"
|
||||
template_name = ("horizon/common/_detail_table.html")
|
||||
preload = False
|
||||
@ -223,8 +225,7 @@ class CGroupsTab(tabs.TableTab, VolumeTableMixIn):
|
||||
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 "
|
||||
@ -232,7 +233,32 @@ class CGroupsTab(tabs.TableTab, VolumeTableMixIn):
|
||||
return cgroups
|
||||
|
||||
|
||||
class CGSnapshotsTab(tabs.TableTab):
|
||||
table_classes = (cg_snapshots_tables.CGSnapshotsTable,)
|
||||
name = _("Consistency Group Snapshots")
|
||||
slug = "cg_snapshots_tab"
|
||||
template_name = ("horizon/common/_detail_table.html")
|
||||
preload = False
|
||||
|
||||
def allowed(self, request):
|
||||
return policy.check(
|
||||
(("volume", "consistencygroup:get_all_cgsnapshots"),),
|
||||
request
|
||||
)
|
||||
|
||||
def get_volume_cg_snapshots_data(self):
|
||||
try:
|
||||
cg_snapshots = api.cinder.volume_cg_snapshot_list(
|
||||
self.request)
|
||||
except Exception:
|
||||
cg_snapshots = []
|
||||
exceptions.handle(self.request, _("Unable to retrieve "
|
||||
"volume consistency group "
|
||||
"snapshots."))
|
||||
return cg_snapshots
|
||||
|
||||
|
||||
class VolumeAndSnapshotTabs(tabs.TabGroup):
|
||||
slug = "volumes_and_snapshots"
|
||||
tabs = (VolumeTab, SnapshotTab, BackupsTab, CGroupsTab)
|
||||
tabs = (VolumeTab, SnapshotTab, BackupsTab, CGroupsTab, CGSnapshotsTab)
|
||||
sticky = True
|
||||
|
@ -0,0 +1,63 @@
|
||||
{% load i18n horizon humanize bootstrap %}
|
||||
|
||||
<h3>{% block head %}{% trans "Volume Limits" %}{% endblock %}</h3>
|
||||
|
||||
<div class="quota_title">
|
||||
<div class="pull-left">
|
||||
<strong>{% trans "Total Gibibytes" %}</strong>
|
||||
<span>({% block gigabytes_used %}{{ usages.gigabytesUsed|intcomma }}{% endblock %} {% trans "GiB" %})</span>
|
||||
</div>
|
||||
<span class="pull-right">{{ usages.maxTotalVolumeGigabytes|intcomma|quota:_("GiB") }}</span>
|
||||
</div>
|
||||
|
||||
{{ minifyspace }}
|
||||
<div id="quota_size"
|
||||
data-progress-indicator-for="id_size"
|
||||
data-quota-limit="{{ usages.maxTotalVolumeGigabytes }}"
|
||||
data-quota-used={% block gigabytes_used_progress %}"{{ usages.gigabytesUsed }}"{% endblock %}
|
||||
class="quota_bar">
|
||||
{% widthratio usages.gigabytesUsed usages.maxTotalVolumeGigabytes 100 as gigabytes_percent %}
|
||||
{% bs_progress_bar gigabytes_percent 0 %}
|
||||
</div>
|
||||
{{ endminifyspace }}
|
||||
|
||||
<div class="quota_title">
|
||||
<div class="pull-left">
|
||||
<strong>{% block type_title %}{% trans "Number of Volumes" %}{% endblock %}</strong>
|
||||
<span>({% block used %}{{ usages.volumesUsed|intcomma }}{% endblock %})</span>
|
||||
</div>
|
||||
<span class="pull-right">{% block total %}{{ usages.maxTotalVolumes|intcomma|quota }}{% endblock %}</span>
|
||||
</div>
|
||||
|
||||
{{ minifyspace }}
|
||||
<div id={% block type_id %}"quota_volumes"{% endblock %}
|
||||
data-quota-limit={% block total_progress %}"{{ usages.maxTotalVolumes }}"{% endblock %}
|
||||
data-quota-used={% block used_progress %}"{{ usages.volumesUsed }}"{% endblock %}
|
||||
class="quota_bar">
|
||||
{% widthratio usages.volumesUsed usages.maxTotalVolumes 100 as volumes_percent %}
|
||||
{% if usages.numRequestedItems %}
|
||||
{% widthratio 100 usages.maxTotalVolumes usages.numRequestedItems as single_step %}
|
||||
{% else %}
|
||||
{% widthratio 100 usages.maxTotalVolumes 1 as single_step %}
|
||||
{% endif %}
|
||||
{% bs_progress_bar volumes_percent single_step %}
|
||||
</div>
|
||||
{{ endminifyspace }}
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
if(typeof horizon.Quota !== 'undefined') {
|
||||
horizon.Quota.init();
|
||||
} else {
|
||||
addHorizonLoadEvent(function() {
|
||||
horizon.Quota.init();
|
||||
});
|
||||
}
|
||||
|
||||
if(typeof horizon.Volumes !== 'undefined') {
|
||||
horizon.Volumes.initWithTypes({{ volume_types|safe|default:"{}" }});
|
||||
} else {
|
||||
addHorizonLoadEvent(function() {
|
||||
horizon.Volumes.initWithTypes({{ volume_types|safe|default:"{}" }});
|
||||
});
|
||||
}
|
||||
</script>
|
@ -0,0 +1,9 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-body-right %}
|
||||
<div class="quota-dynamic">
|
||||
<p>{% blocktrans %}Create a Consistency Group that will contain newly created volumes cloned from each of the snapshots in the source Consistency Group Snapshot.{% endblocktrans %}</p>
|
||||
{% include "project/volumes/_volume_limits.html" with usages=usages %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,46 @@
|
||||
{% load i18n sizeformat parse_date %}
|
||||
|
||||
<div class="detail">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ cg_snapshot.name }}</dd>
|
||||
<dt>{% trans "ID" %}</dt>
|
||||
<dd>{{ cg_snapshot.id }}</dd>
|
||||
{% if cg_snapshot.description %}
|
||||
<dt>{% trans "Description" %}</dt>
|
||||
<dd>{{ cg_snapshot.description }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>{{ cg_snapshot.status|capfirst }}</dd>
|
||||
<dt>{% trans "Consistency Group" %}</dt>
|
||||
<dd>
|
||||
<a href="{% url 'horizon:project:volumes:cgroups:detail' cg_snapshot.consistencygroup_id %}">
|
||||
{% if cg_snapshot.cg_name %}
|
||||
{{ cg_snapshot.cg_name }}
|
||||
{% else %}
|
||||
{{ cg_snapshot.consistencygroup_id }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<h4>{% trans "Snapshot Volume Types" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl class="dl-horizontal">
|
||||
{% for vol_type_names in cg_snapshot.volume_type_names %}
|
||||
<dd>{{ vol_type_names }}</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
|
||||
<h4>{% trans "Snapshot Volumes" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl class="dl-horizontal">
|
||||
{% for vol_names in cg_snapshot.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 snapshot." %}</p>
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/cg_snapshots/_create.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/cg_snapshots/_update.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,9 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-body-right %}
|
||||
<div class="quota-dynamic">
|
||||
<p>{% blocktrans %}Clone each of the volumes in the source Consistency Group, and then add them to a newly created Consistency Group.{% endblocktrans %}</p>
|
||||
{% include "project/volumes/_volume_limits.html" with usages=usages snapshot_quota=False %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,10 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-body-right %}
|
||||
<div class="quota-dynamic">
|
||||
<p>{% blocktrans %}Create a snapshot for each volume contained in the Consistency Group.{% endblocktrans %}</p>
|
||||
<p>{% blocktrans %}Snapshots can only be created for Consistency Groups that contain volumes.{% endblocktrans %}</p>
|
||||
{% include "project/volumes/cgroups/_snapshot_limits.html" with usages=usages snapshot_quota=True %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -3,8 +3,7 @@
|
||||
{% 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 "Volume consistency groups can not be deleted if they contain volumes." %}</p>
|
||||
<p>{% trans "Check the "Delete Volumes" box to also delete any 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,7 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<p>{% trans "This action will unassign all volumes that are currently contained in this consistency group." %}</p>
|
||||
{% endblock %}
|
@ -0,0 +1,42 @@
|
||||
{% extends "project/volumes/_volume_limits.html" %}
|
||||
{% load i18n horizon humanize %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "From here you can create a snapshot of a volume." %}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{% trans "Snapshot Limits" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block gigabytes_used %}
|
||||
{{ usages.totalGigabytesUsed|intcomma }}
|
||||
{% endblock %}
|
||||
|
||||
{% block gigabytes_used_progress %}
|
||||
"{{ usages.totalGigabytesUsed }}"
|
||||
{% endblock %}
|
||||
|
||||
{% block type_title %}
|
||||
{% trans "Number of Snapshots" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block used %}
|
||||
{{ usages.snapshotsUsed|intcomma }}
|
||||
{% endblock %}
|
||||
|
||||
{% block total %}
|
||||
{{ usages.maxTotalSnapshots|intcomma|quota }}
|
||||
{% endblock %}
|
||||
|
||||
{% block type_id %}
|
||||
"quota_snapshots"
|
||||
{% endblock %}
|
||||
|
||||
{% block total_progress %}
|
||||
"{{ usages.maxTotalSnapshots }}"
|
||||
{% endblock %}
|
||||
|
||||
{% block used_progress %}
|
||||
"{{ usages.snapshotsUsed }}"
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/cgroups/_clone_cgroup.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/cgroups/_create_snapshot.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/cgroups/_remove_vols.html' %}
|
||||
{% endblock %}
|
@ -47,7 +47,11 @@
|
||||
data-quota-used={% block used_progress %}"{{ usages.volumesUsed }}"{% endblock %}
|
||||
class="quota_bar">
|
||||
{% widthratio usages.volumesUsed usages.maxTotalVolumes 100 as volumes_percent %}
|
||||
{% if usages.numRequestedItems %}
|
||||
{% widthratio 100 usages.maxTotalVolumes usages.numRequestedItems as single_step %}
|
||||
{% else %}
|
||||
{% widthratio 100 usages.maxTotalVolumes 1 as single_step %}
|
||||
{% endif %}
|
||||
{% bs_progress_bar volumes_percent single_step %}
|
||||
</div>
|
||||
{{ endminifyspace }}
|
||||
|
@ -17,6 +17,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.cg_snapshots \
|
||||
import urls as cg_snapshots_urls
|
||||
from openstack_dashboard.dashboards.project.volumes.cgroups \
|
||||
import urls as cgroup_urls
|
||||
from openstack_dashboard.dashboards.project.volumes.snapshots \
|
||||
@ -35,8 +37,21 @@ urlpatterns = [
|
||||
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')),
|
||||
url(r'^\?tab=volumes_and_snapshots__cg_snapshots_tab$',
|
||||
views.IndexView.as_view(), name='cg_snapshots_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')),
|
||||
url(r'cg_snapshots/', include(
|
||||
cg_snapshots_urls,
|
||||
namespace='cg_snapshots')),
|
||||
]
|
||||
|
@ -111,6 +111,10 @@ class DeleteVolume(VolumePolicyTargetMixin, tables.DeleteAction):
|
||||
|
||||
def allowed(self, request, volume=None):
|
||||
if volume:
|
||||
# Can't delete volume if part of consistency group
|
||||
if getattr(volume, 'consistencygroup_id', None):
|
||||
return False
|
||||
|
||||
return (volume.status in DELETABLE_STATES and
|
||||
not getattr(volume, 'has_snapshot', False))
|
||||
return True
|
||||
|
@ -356,15 +356,13 @@ class CinderApiTests(test.APITestCase):
|
||||
|
||||
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)
|
||||
cinderclient.volume_types.list().AndReturn(volume_types_list)
|
||||
self.mox.ReplayAll()
|
||||
api_cgroups = api.cinder.volume_cgroup_list_with_vol_type_names(
|
||||
self.request)
|
||||
@ -373,6 +371,29 @@ class CinderApiTests(test.APITestCase):
|
||||
self.assertEqual(volume_types_list[i].name,
|
||||
api_cgroups[0].volume_type_names[i])
|
||||
|
||||
def test_cgsnapshot_list(self):
|
||||
cgsnapshots = self.cinder_cg_snapshots.list()
|
||||
cinderclient = self.stub_cinderclient()
|
||||
cinderclient.cgsnapshots = self.mox.CreateMockAnything()
|
||||
cinderclient.cgsnapshots.list(search_opts=None).\
|
||||
AndReturn(cgsnapshots)
|
||||
self.mox.ReplayAll()
|
||||
api_cgsnapshots = api.cinder.volume_cg_snapshot_list(self.request)
|
||||
self.assertEqual(len(cgsnapshots), len(api_cgsnapshots))
|
||||
|
||||
def test_cgsnapshot_get(self):
|
||||
cgsnapshot = self.cinder_cg_snapshots.first()
|
||||
cinderclient = self.stub_cinderclient()
|
||||
cinderclient.cgsnapshots = self.mox.CreateMockAnything()
|
||||
cinderclient.cgsnapshots.get(cgsnapshot.id).AndReturn(cgsnapshot)
|
||||
self.mox.ReplayAll()
|
||||
api_cgsnapshot = api.cinder.volume_cg_snapshot_get(self.request,
|
||||
cgsnapshot.id)
|
||||
self.assertEqual(api_cgsnapshot.name, cgsnapshot.name)
|
||||
self.assertEqual(api_cgsnapshot.description, cgsnapshot.description)
|
||||
self.assertEqual(api_cgsnapshot.consistencygroup_id,
|
||||
cgsnapshot.consistencygroup_id)
|
||||
|
||||
|
||||
class CinderApiVersionTests(test.TestCase):
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
# under the License.
|
||||
|
||||
from cinderclient.v2 import availability_zones
|
||||
from cinderclient.v2 import cgsnapshots
|
||||
from cinderclient.v2 import consistencygroups
|
||||
from cinderclient.v2 import pools
|
||||
from cinderclient.v2 import qos_specs
|
||||
@ -49,6 +50,7 @@ def data(TEST):
|
||||
TEST.cinder_pools = utils.TestDataContainer()
|
||||
TEST.cinder_consistencygroups = utils.TestDataContainer()
|
||||
TEST.cinder_cgroup_volumes = utils.TestDataContainer()
|
||||
TEST.cinder_cg_snapshots = utils.TestDataContainer()
|
||||
|
||||
# Services
|
||||
service_1 = services.Service(services.ServiceManager(None), {
|
||||
@ -148,7 +150,9 @@ def data(TEST):
|
||||
{'id': u'1',
|
||||
'name': u'vol_type_1',
|
||||
'description': 'type 1 description',
|
||||
'extra_specs': {'foo': 'bar'}})
|
||||
'extra_specs': {'foo': 'bar',
|
||||
'volume_backend_name':
|
||||
'backend_1'}})
|
||||
vol_type2 = volume_types.VolumeType(volume_types.VolumeTypeManager(None),
|
||||
{'id': u'2',
|
||||
'name': u'vol_type_2',
|
||||
@ -419,3 +423,12 @@ def data(TEST):
|
||||
'consistencygroup_id': u'1'})
|
||||
TEST.cinder_cgroup_volumes.add(api.cinder.Volume(
|
||||
volume_for_consistency_group))
|
||||
|
||||
# volume consistency group snapshots
|
||||
cg_snapshot_1 = cgsnapshots.Cgsnapshot(
|
||||
cgsnapshots.CgsnapshotManager(None),
|
||||
{'id': u'1',
|
||||
'name': u'cg_ss_1',
|
||||
'description': 'cg_ss 1 description',
|
||||
'consistencygroup_id': u'1'})
|
||||
TEST.cinder_cg_snapshots.add(cg_snapshot_1)
|
||||
|
@ -0,0 +1,13 @@
|
||||
---
|
||||
features:
|
||||
- >
|
||||
[`blueprint cinder-consistency-groups <https://blueprints.launchpad.net/horizon/+spec/cinder-consistency-groups>`_]
|
||||
This feature adds 2 new tabs to the Project Volumes panel. The first tab will display
|
||||
Consistency Groups, and the second tab will display Consistency Group Snapshots.
|
||||
Consistency Groups (CG) contain existing volumes, and allow the user to perform
|
||||
actions on the volumes in one step. Actions include: create/update/delete CGs,
|
||||
snapshot all volumes in a CG, clone all volumes in a CG, and create a new CG and
|
||||
volumes from a CG snapshot.
|
||||
|
||||
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