Drop cinder consistency group support

Cinder consistency group has been replaced by the generic group feature.
Horizon support of the generic group (in the project dashboard) is
available since Rocky release and it covers all existing support
for consistency group in horizon.

The consistency group support is horizon was marked as deprecated
in Stein release [1].

This commit drops the consistency group support.

[1] https://review.openstack.org/#/c/626846/

Change-Id: I11187d2b03b7e0033a6c6ba3f8be25b8b5e4dd74
This commit is contained in:
Akihiro Motoki 2019-03-22 18:16:26 +09:00
parent 4ab7f806b8
commit 431fd6c16b
48 changed files with 10 additions and 2776 deletions

View File

@ -120,18 +120,6 @@ class VolumeType(BaseCinderAPIResourceWrapper):
'os-extended-snapshot-attributes:project_id']
class VolumeConsistencyGroup(BaseCinderAPIResourceWrapper):
_attrs = ['id', 'name', 'description', 'status', 'availability_zone',
'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',
@ -583,123 +571,6 @@ def volume_snapshot_reset_state(request, snapshot_id, state):
snapshot_id, state)
@profiler.trace
def volume_cgroup_get(request, cgroup_id):
cgroup = cinderclient(request).consistencygroups.get(cgroup_id)
return VolumeConsistencyGroup(cgroup)
@profiler.trace
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
@profiler.trace
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)]
@profiler.trace
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:
for vol_type in vol_types:
if vol_type.id == vol_type_id:
cgroup.volume_type_names.append(vol_type.name)
break
return cgroups
@profiler.trace
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)
@profiler.trace
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_from_src(
cg_snapshot_id,
source_cgroup_id,
name,
description,
user_id,
project_id))
@profiler.trace
def volume_cgroup_delete(request, cgroup_id, force=False):
return cinderclient(request).consistencygroups.delete(cgroup_id, force)
@profiler.trace
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)
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."""

View File

@ -40,7 +40,6 @@ MICROVERSION_FEATURES = {
},
"cinder": {
"groups": ["3.27", "3.43", "3.48", "3.58"],
"consistency_groups": ["2.0", "3.10"],
"message_list": ["3.5", "3.29"],
"limits_project_id_query": ["3.43", "3.50", "3.55"],
}

View File

@ -1,10 +0,0 @@
# extra policies for consistency group
"consistencygroup:create" : ""
"consistencygroup:create_cgsnapshot" : ""
"consistencygroup:delete": ""
"consistencygroup:delete_cgsnapshot": ""
"consistencygroup:get": ""
"consistencygroup:get_all": ""
"consistencygroup:get_all_cgsnapshots": ""
"consistencygroup:get_cgsnapshot": ""
"consistencygroup:update": ""

View File

@ -71,7 +71,7 @@ class AdminVolumeGroupTests(test.BaseAdminViewTests):
@test.create_mocks({api.cinder: ['group_get', 'group_delete']})
def test_delete_group_delete_volumes_flag(self):
group = self.cinder_consistencygroups.first()
group = self.cinder_groups.first()
formData = {'delete_volumes': True}
self.mock_group_get.return_value = group

View File

@ -1,76 +0,0 @@
# 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.urls 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.ThemableSelectWidget(
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:cg_snapshots:index")
msg = _('Unable to create consistency '
'group "%s" from snapshot.') % data['name']
exceptions.handle(request,
msg,
redirect=redirect)

View File

@ -1,51 +0,0 @@
# Copyright 2017 Rackspace, Inc.
#
# 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.
import logging
from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard import api
from openstack_dashboard import policy
LOG = logging.getLogger(__name__)
class CGSnapshots(horizon.Panel):
name = _("Consistency Group Snapshots")
slug = 'cg_snapshots'
permissions = (
('openstack.services.volume', 'openstack.services.volumev2',
'openstack.services.volumev3'),
)
policy_rules = (("volume", "consistencygroup:get_all_cgsnapshots"),)
def allowed(self, context):
request = context['request']
try:
return (
super(CGSnapshots, self).allowed(context) and
request.user.has_perms(self.permissions) and
policy.check(self.policy_rules, request) and
api.cinder.get_microversion(request, 'consistency_groups') and
not api.cinder.get_microversion(request, 'groups')
)
except Exception:
LOG.error("Call to list enabled services failed. This is likely "
"due to a problem communicating with the Cinder "
"endpoint. Consistency Group Snapshot panel will not be "
"displayed.")
return False

View File

@ -1,118 +0,0 @@
# 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: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: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', 'openstack.services.volumev2',
'openstack.services.volumev3')
]

View File

@ -1,34 +0,0 @@
# 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.urls 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/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:cg_snapshots:index')
class CGSnapshotsDetailTabs(tabs.TabGroup):
slug = "cg_snapshots_details"
tabs = (OverviewTab,)

View File

@ -1,9 +0,0 @@
{% 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 %}

View File

@ -1,46 +0,0 @@
{% load i18n sizeformat parse_date %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd data-display="{{ cg_snapshot.name|default:cg_snapshot.id }}" class="word-wrap">{{ 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 class="word-wrap">
<a href="{% url 'horizon:project: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 class="word-wrap">{{ 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 class="word-wrap">{{ vol_names }}</dd>
{% empty %}
<dd>
<em>{% trans "No assigned volumes" %}</em>
</dd>
{% endfor %}
</dl>
</div>

View File

@ -1,7 +0,0 @@
{% 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 %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% include 'project/cg_snapshots/_create.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% include 'project/cg_snapshots/_update.html' %}
{% endblock %}

View File

@ -1,174 +0,0 @@
# 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.urls import reverse
import mock
from openstack_dashboard.api import cinder
from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:project:cg_snapshots:index')
class CGroupSnapshotTests(test.TestCase):
@test.create_mocks({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'}
self.mock_volume_cg_snapshot_get.return_value = cg_snapshot
self.mock_volume_cgroup_create_from_source.return_value = cgroup
url = reverse('horizon:project:cg_snapshots:create_cgroup',
args=[cg_snapshot.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(
res, reverse('horizon:project:cgroups:index'))
self.mock_volume_cg_snapshot_get.assert_called_once_with(
test.IsHttpRequest(), cg_snapshot.id)
self.mock_volume_cgroup_create_from_source.assert_called_once_with(
test.IsHttpRequest(),
formData['name'],
cg_snapshot_id=formData['cg_snapshot_id'],
description=formData['description'])
@test.create_mocks({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'}
self.mock_volume_cg_snapshot_get.return_value = cg_snapshot
self.mock_volume_cgroup_create_from_source.side_effect = \
self.exceptions.cinder
url = reverse('horizon:project: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, INDEX_URL)
self.mock_volume_cg_snapshot_get.assert_called_once_with(
test.IsHttpRequest(), cg_snapshot.id)
self.mock_volume_cgroup_create_from_source.assert_called_once_with(
test.IsHttpRequest(),
formData['name'],
cg_snapshot_id=formData['cg_snapshot_id'],
description=formData['description'])
@test.create_mocks({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()
self.mock_volume_cg_snapshot_list.return_value = cg_snapshots
self.mock_volume_cg_snapshot_delete.return_value = None
form_data = {'action': 'volume_cg_snapshots__delete_cg_snapshot__%s'
% cg_snapshot.id}
res = self.client.post(INDEX_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']])
self.assert_mock_multiple_calls_with_same_arguments(
self.mock_volume_cg_snapshot_list, 2,
mock.call(test.IsHttpRequest()))
self.mock_volume_cg_snapshot_delete.assert_called_once_with(
test.IsHttpRequest(), cg_snapshot.id)
@test.create_mocks({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()
self.mock_volume_cg_snapshot_list.return_value = cg_snapshots
self.mock_volume_cg_snapshot_delete.side_effect = \
self.exceptions.cinder
form_data = {'action': 'volume_cg_snapshots__delete_cg_snapshot__%s'
% cg_snapshot.id}
res = self.client.post(INDEX_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']])
self.assert_mock_multiple_calls_with_same_arguments(
self.mock_volume_cg_snapshot_list, 2,
mock.call(test.IsHttpRequest()))
self.mock_volume_cg_snapshot_delete.assert_called_once_with(
test.IsHttpRequest(), cg_snapshot.id)
@test.create_mocks({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()
self.mock_volume_cg_snapshot_get.return_value = cg_snapshot
self.mock_volume_cgroup_get.return_value = cgroup
self.mock_volume_type_get.return_value = volume_type
self.mock_volume_list.return_value = volumes
url = reverse(
'horizon:project:cg_snapshots:cg_snapshot_detail',
args=[cg_snapshot.id])
res = self.client.get(url)
self.assertNoFormErrors(res)
self.assertEqual(res.status_code, 200)
self.mock_volume_cg_snapshot_get.assert_called_once_with(
test.IsHttpRequest(), cg_snapshot.id)
self.mock_volume_cgroup_get.assert_called_once_with(
test.IsHttpRequest(), cgroup.id)
self.mock_volume_type_get.assert_called_once_with(
test.IsHttpRequest(), volume_type.id)
search_opts = {'consistencygroup_id': cgroup.id}
self.mock_volume_list.assert_called_once_with(
test.IsHttpRequest(), search_opts=search_opts)
@test.create_mocks({cinder: ('volume_cg_snapshot_get',)})
def test_detail_view_with_exception(self):
cg_snapshot = self.cinder_cg_snapshots.first()
self.mock_volume_cg_snapshot_get.side_effect = self.exceptions.cinder
url = reverse(
'horizon:project:cg_snapshots:cg_snapshot_detail',
args=[cg_snapshot.id])
res = self.client.get(url)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_volume_cg_snapshot_get.assert_called_once_with(
test.IsHttpRequest(), cg_snapshot.id)

View File

@ -1,25 +0,0 @@
# 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 url
from openstack_dashboard.dashboards.project.cg_snapshots import views
urlpatterns = [
url(r'^$', views.CGSnapshotsView.as_view(), name='index'),
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'),
]

View File

@ -1,153 +0,0 @@
# 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.urls import reverse
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import tables
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.cg_snapshots \
import forms as cg_snapshot_forms
from openstack_dashboard.dashboards.project.cg_snapshots \
import tables as cg_snapshot_tables
from openstack_dashboard.dashboards.project.cg_snapshots \
import tabs as cg_snapshot_tabs
CGROUP_INFO_FIELDS = ("name",
"description")
INDEX_URL = "horizon:project:cg_snapshots:index"
class CGSnapshotsView(tables.DataTableView):
table_class = cg_snapshot_tables.CGSnapshotsTable
page_title = _("Consistency Group Snapshots")
def get_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 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(INDEX_URL)
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
template_name = 'project/cg_snapshots/create.html'
submit_url = "horizon:project:cg_snapshots:create_cgroup"
success_url = reverse_lazy('horizon:project:cgroups:index')
page_title = _("Create Volume Consistency Group")
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_quota_usages(
self.request, targets=('volumes', 'gigabytes'))
if (usages['volumes']['used'] + num_volumes >
usages['volumes']['quota']):
raise ValueError(_('Unable to create consistency group due to '
'exceeding volume quota limit.'))
else:
context['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"]}

View File

@ -1,216 +0,0 @@
# 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.urls 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 clean(self):
cleaned_data = super(UpdateForm, self).clean()
new_desc = cleaned_data.get('description')
old_desc = self.initial['description']
if old_desc and not new_desc:
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,
data['name'],
data['description'])
message = _('Updating volume consistency '
'group "%s"') % data['name']
messages.info(request, message)
return True
except Exception:
redirect = reverse("horizon:project:cgroups:index")
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:cgroups: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:cgroups: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:cgroups: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 not volumes:
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.ThemableSelectWidget(
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:cgroups:index")
msg = _('Unable to clone consistency group.')
search_opts = {'consistentcygroup_id': data['cgroup_id']}
volumes = cinder.volume_list(request, search_opts=search_opts)
if not volumes:
msg = _('Unable to clone empty consistency group.')
exceptions.handle(request,
msg,
redirect=redirect)

View File

@ -1,51 +0,0 @@
# Copyright 2017 Rackspace, Inc.
#
# 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.
import logging
from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard import api
from openstack_dashboard import policy
LOG = logging.getLogger(__name__)
class CGroups(horizon.Panel):
name = _("Consistency Groups")
slug = 'cgroups'
permissions = (
('openstack.services.volume', 'openstack.services.volumev2',
'openstack.services.volumev3'),
)
policy_rules = (("volume", "consistencygroup:get_all"),)
def allowed(self, context):
request = context['request']
try:
return (
super(CGroups, self).allowed(context) and
request.user.has_perms(self.permissions) and
policy.check(self.policy_rules, request) and
api.cinder.get_microversion(request, 'consistency_groups') and
not api.cinder.get_microversion(request, 'groups')
)
except Exception:
LOG.error("Call to list enabled services failed. This is likely "
"due to a problem communicating with the Cinder "
"endpoint. Consistency Group panel will not be "
"displayed.")
return False

View File

@ -1,175 +0,0 @@
# 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 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:cgroups:create"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("volume", "consistencygroup:create"),)
class DeleteVolumeCGroup(policy.PolicyTargetMixin, tables.LinkAction):
name = "deletecg"
verbose_name = _("Delete Consistency Group")
url = "horizon:project:cgroups:delete"
classes = ("ajax-modal", "btn-danger")
policy_rules = (("volume", "consistencygroup:delete"), )
class RemoveAllVolumes(policy.PolicyTargetMixin, tables.LinkAction):
name = "remove_vols"
verbose_name = _("Remove Volumes from Consistency Group")
url = "horizon:project:cgroups:remove_volumes"
classes = ("ajax-modal",)
policy_rules = (("volume", "consistencygroup:update"), )
class EditVolumeCGroup(policy.PolicyTargetMixin, tables.LinkAction):
name = "edit"
verbose_name = _("Edit Consistency Group")
url = "horizon:project: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:cgroups:manage"
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: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: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):
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
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 = ''
if hasattr(cgroup, 'volume_type_names'):
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.WrappingColumn("name",
verbose_name=_("Name"),
link="horizon:project: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,
CreateSnapshot,
CloneCGroup,
RemoveAllVolumes,
DeleteVolumeCGroup)
row_class = UpdateRow
status_columns = ("status",)
permissions = ['openstack.services.volume']

View File

@ -1,34 +0,0 @@
# 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.urls 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/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:cgroups:index')
class CGroupsDetailTabs(tabs.TabGroup):
slug = "cgroup_details"
tabs = (OverviewTab,)

View File

@ -1,9 +0,0 @@
{% 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 %}

View File

@ -1,10 +0,0 @@
{% 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/cgroups/_snapshot_limits.html" with usages=usages snapshot_quota=True %}
</div>
{% endblock %}

View File

@ -1,9 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block modal-body-right %}
<p>{% trans "Volume consistency groups can not be deleted if they contain volumes." %}</p>
<p>{% trans "Check the &quot;Delete Volumes&quot; 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 &quot;attached&quot; or has any dependent snapshots." %}</p>
{% endblock %}

View File

@ -1,34 +0,0 @@
{% load i18n sizeformat parse_date %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd class="word-wrap">{{ 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 class="word-wrap">{{ 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 class="word-wrap">{{ vol_names }}</dd>
{% empty %}
<dd>
<em>{% trans "No assigned volumes" %}</em>
</dd>
{% endfor %}
</dl>
</div>

View File

@ -1,7 +0,0 @@
{% 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 %}

View File

@ -1,42 +0,0 @@
{% 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.gigabytes.used|intcomma }}
{% endblock %}
{% block gigabytes_used_progress %}
"{{ usages.gigabytes.used }}"
{% endblock %}
{% block type_title %}
{% trans "Number of Snapshots" %}
{% endblock %}
{% block used %}
{{ usages.snapshots.used|intcomma }}
{% endblock %}
{% block total %}
{{ usages.snapshots.quota|intcomma|quota }}
{% endblock %}
{% block type_id %}
"quota_snapshots"
{% endblock %}
{% block total_progress %}
"{{ usages.snapshots.quota }}"
{% endblock %}
{% block used_progress %}
"{{ usages.snapshots.used }}"
{% endblock %}

View File

@ -1,7 +0,0 @@
{% 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 %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% include 'project/cgroups/_clone_cgroup.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% include 'horizon/common/_workflow.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% include 'project/cgroups/_create_snapshot.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% include 'project/cgroups/_delete.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% include 'project/cgroups/_remove_vols.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% include 'project/cgroups/_update.html' %}
{% endblock %}

View File

@ -1,355 +0,0 @@
# 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.urls import reverse
from django.utils.http import urlunquote
import mock
from openstack_dashboard.api import cinder
from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:project:cgroups:index')
VOLUME_CGROUPS_SNAP_INDEX_URL = urlunquote(reverse(
'horizon:project:cg_snapshots:index'))
class ConsistencyGroupTests(test.TestCase):
@test.create_mocks({cinder: ('extension_supported',
'availability_zone_list',
'volume_type_list',
'volume_type_list_with_qos_associations',
'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,
'add_vtypes_to_cgroup_role_member': [volume_type_id]}
self.mock_extension_supported.return_value = True
self.mock_availability_zone_list.return_value = \
self.cinder_availability_zones.list()
self.mock_volume_type_list.return_value = volume_types
self.mock_volume_type_list_with_qos_associations.return_value = \
volume_types
self.mock_volume_cgroup_list.return_value = \
self.cinder_consistencygroups.list()
self.mock_volume_cgroup_create.return_value = cgroup
url = reverse('horizon:project:cgroups:create')
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_extension_supported.assert_called_once_with(
test.IsHttpRequest(), 'AvailabilityZones')
self.mock_availability_zone_list.assert_called_once_with(
test.IsHttpRequest())
self.mock_volume_type_list.assert_called_once_with(
test.IsHttpRequest())
self.mock_volume_type_list_with_qos_associations \
.assert_called_once_with(test.IsHttpRequest())
self.mock_volume_cgroup_list.assert_called_once_with(
test.IsHttpRequest())
self.mock_volume_cgroup_create.assert_called_once_with(
test.IsHttpRequest(),
formData['volume_types'],
formData['name'],
description=formData['description'],
availability_zone=formData['availability_zone'])
@test.create_mocks({cinder: ('extension_supported',
'availability_zone_list',
'volume_type_list',
'volume_type_list_with_qos_associations',
'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,
'add_vtypes_to_cgroup_role_member': [volume_type_id]}
self.mock_extension_supported.return_value = True
self.mock_availability_zone_list.return_value = \
self.cinder_availability_zones.list()
self.mock_volume_type_list.return_value = volume_types
self.mock_volume_type_list_with_qos_associations.return_value = \
volume_types
self.mock_volume_cgroup_list.return_value = \
self.cinder_consistencygroups.list()
self.mock_volume_cgroup_create.side_effect = self.exceptions.cinder
url = reverse('horizon:project:cgroups:create')
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertIn("Unable to create consistency group.",
res.cookies.output())
self.mock_extension_supported.assert_called_once_with(
test.IsHttpRequest(), 'AvailabilityZones')
self.mock_availability_zone_list.assert_called_once_with(
test.IsHttpRequest())
self.mock_volume_type_list.assert_called_once_with(
test.IsHttpRequest())
self.mock_volume_type_list_with_qos_associations \
.assert_called_once_with(test.IsHttpRequest())
self.mock_volume_cgroup_list.assert_called_once_with(
test.IsHttpRequest())
self.mock_volume_cgroup_create.assert_called_once_with(
test.IsHttpRequest(),
formData['volume_types'],
formData['name'],
description=formData['description'],
availability_zone=formData['availability_zone'])
@test.create_mocks({cinder: ('volume_cgroup_get',
'volume_cgroup_delete')})
def test_delete_cgroup(self):
cgroup = self.cinder_consistencygroups.first()
self.mock_volume_cgroup_get.return_value = cgroup
self.mock_volume_cgroup_delete.return_value = None
url = reverse('horizon:project:cgroups:delete',
args=[cgroup.id])
res = self.client.post(url)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_volume_cgroup_get.assert_called_once_with(
test.IsHttpRequest(), cgroup.id)
self.mock_volume_cgroup_delete.assert_called_once_with(
test.IsHttpRequest(), cgroup.id, force=False)
@test.create_mocks({cinder: ('volume_cgroup_get',
'volume_cgroup_delete')})
def test_delete_cgroup_force_flag(self):
cgroup = self.cinder_consistencygroups.first()
formData = {'delete_volumes': True}
self.mock_volume_cgroup_get.return_value = cgroup
self.mock_volume_cgroup_delete.return_value = None
url = reverse('horizon:project:cgroups:delete',
args=[cgroup.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_volume_cgroup_get.assert_called_once_with(
test.IsHttpRequest(), cgroup.id)
self.mock_volume_cgroup_delete.assert_called_once_with(
test.IsHttpRequest(), cgroup.id, force=True)
@test.create_mocks({cinder: ('volume_cgroup_get',
'volume_cgroup_delete')})
def test_delete_cgroup_exception(self):
cgroup = self.cinder_consistencygroups.first()
formData = {'delete_volumes': False}
self.mock_volume_cgroup_get.return_value = cgroup
self.mock_volume_cgroup_delete.side_effect = self.exceptions.cinder
url = reverse('horizon:project:cgroups:delete',
args=[cgroup.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_volume_cgroup_get.assert_called_once_with(
test.IsHttpRequest(), cgroup.id)
self.mock_volume_cgroup_delete.assert_called_once_with(
test.IsHttpRequest(), cgroup.id, force=False)
def test_update_cgroup_add_vol(self):
self._test_update_cgroup_add_remove_vol(add=True)
def test_update_cgroup_remove_vol(self):
self._test_update_cgroup_add_remove_vol(add=False)
@test.create_mocks({cinder: ('volume_list',
'volume_type_list',
'volume_cgroup_get',
'volume_cgroup_update')})
def _test_update_cgroup_add_remove_vol(self, add=True):
cgroup = self.cinder_consistencygroups.first()
volume_types = self.cinder_volume_types.list()
volumes = (self.cinder_volumes.list() +
self.cinder_cgroup_volumes.list())
cgroup_voltype_names = [t.name for t in volume_types
if t.id in cgroup.volume_types]
compat_volumes = [v for v in volumes
if v.volume_type in cgroup_voltype_names]
compat_volume_ids = [v.id for v in compat_volumes]
assigned_volume_ids = [v.id for v in compat_volumes
if getattr(v, 'consistencygroup_id', None)]
add_volume_ids = [v.id for v in compat_volumes
if v.id not in assigned_volume_ids]
new_volums = compat_volume_ids if add else []
formData = {
'default_add_volumes_to_cgroup_role': 'member',
'add_volumes_to_cgroup_role_member': new_volums,
}
self.mock_volume_list.return_value = volumes
self.mock_volume_type_list.return_value = volume_types
self.mock_volume_cgroup_get.return_value = cgroup
self.mock_volume_cgroup_update.return_value = cgroup
url = reverse('horizon:project:cgroups:manage',
args=[cgroup.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assert_mock_multiple_calls_with_same_arguments(
self.mock_volume_list, 2,
mock.call(test.IsHttpRequest()))
self.mock_volume_type_list.assert_called_once_with(
test.IsHttpRequest())
self.mock_volume_cgroup_get.assert_called_once_with(
test.IsHttpRequest(), cgroup.id)
if add:
self.mock_volume_cgroup_update.assert_called_once_with(
test.IsHttpRequest(), cgroup.id,
name=cgroup.name,
add_vols=','.join(add_volume_ids),
remove_vols='')
else:
self.mock_volume_cgroup_update.assert_called_once_with(
test.IsHttpRequest(), cgroup.id,
name=cgroup.name,
add_vols='',
remove_vols=','.join(assigned_volume_ids))
@test.create_mocks({cinder: ('volume_cgroup_get',
'volume_cgroup_update')})
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'}
self.mock_volume_cgroup_get.return_value = cgroup
self.mock_volume_cgroup_update.return_value = cgroup
url = reverse('horizon:project:cgroups:update',
args=[cgroup.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_volume_cgroup_get.assert_called_once_with(
test.IsHttpRequest(), cgroup.id)
self.mock_volume_cgroup_update.assert_called_once_with(
test.IsHttpRequest(), cgroup.id,
formData['name'],
formData['description'])
@test.create_mocks({cinder: ('volume_cgroup_get',
'volume_cgroup_update')})
def test_update_cgroup_with_exception(self):
cgroup = self.cinder_consistencygroups.first()
formData = {'volume_types': '1',
'name': 'test CG-new',
'description': 'test desc-new'}
self.mock_volume_cgroup_get.return_value = cgroup
self.mock_volume_cgroup_update.side_effect = self.exceptions.cinder
url = reverse('horizon:project:cgroups:update',
args=[cgroup.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_volume_cgroup_get.assert_called_once_with(
test.IsHttpRequest(), cgroup.id)
self.mock_volume_cgroup_update.assert_called_once_with(
test.IsHttpRequest(), cgroup.id,
formData['name'],
formData['description'])
@test.create_mocks({cinder: ('volume_cgroup_get',)})
def test_detail_view_with_exception(self):
cgroup = self.cinder_consistencygroups.first()
self.mock_volume_cgroup_get.side_effect = self.exceptions.cinder
url = reverse('horizon:project:cgroups:detail',
args=[cgroup.id])
res = self.client.get(url)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_volume_cgroup_get.assert_called_once_with(
test.IsHttpRequest(), cgroup.id)
@test.create_mocks({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'}
self.mock_volume_cg_snapshot_create.return_value = cg_snapshot
url = reverse('horizon:project:cgroups:create_snapshot',
args=[cgroup.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, VOLUME_CGROUPS_SNAP_INDEX_URL)
self.mock_volume_cg_snapshot_create.assert_called_once_with(
test.IsHttpRequest(),
formData['cgroup_id'],
formData['name'],
formData['description'])
@test.create_mocks({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'}
self.mock_volume_cgroup_get.return_value = cgroup
self.mock_volume_cgroup_create_from_source.return_value = cgroup
url = reverse('horizon:project:cgroups:clone_cgroup',
args=[cgroup.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_volume_cgroup_get.assert_called_once_with(
test.IsHttpRequest(), cgroup.id)
self.mock_volume_cgroup_create_from_source.assert_called_once_with(
test.IsHttpRequest(),
formData['name'],
source_cgroup_id=formData['cgroup_id'],
description=formData['description'])

View File

@ -1,44 +0,0 @@
# 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 url
from openstack_dashboard.dashboards.project.cgroups import views
urlpatterns = [
url(r'^$', views.CGroupsView.as_view(), name='index'),
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>[^/]+)/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'),
]

View File

@ -1,320 +0,0 @@
# 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.urls import reverse
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import tables
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.usage import quotas
from openstack_dashboard.dashboards.project.cgroups \
import forms as vol_cgroup_forms
from openstack_dashboard.dashboards.project.cgroups \
import tables as vol_cgroup_tables
from openstack_dashboard.dashboards.project.cgroups \
import tabs as vol_cgroup_tabs
from openstack_dashboard.dashboards.project.cgroups \
import workflows as vol_cgroup_workflows
CGROUP_INFO_FIELDS = ("name",
"description")
INDEX_URL = "horizon:project:cgroups:index"
class CGroupsView(tables.DataTableView):
table_class = vol_cgroup_tables.VolumeCGroupsTable
page_title = _("Consistency Groups")
def get_data(self):
try:
cgroups = api.cinder.volume_cgroup_list_with_vol_type_names(
self.request)
except Exception:
cgroups = []
exceptions.handle(self.request, _("Unable to retrieve "
"volume consistency groups."))
return cgroups
class CreateView(workflows.WorkflowView):
workflow_class = vol_cgroup_workflows.CreateCGroupWorkflow
template_name = 'project/cgroups/create.html'
page_title = _("Create Volume Consistency Group")
class UpdateView(forms.ModalFormView):
template_name = 'project/cgroups/update.html'
page_title = _("Edit Consistency Group")
form_class = vol_cgroup_forms.UpdateForm
success_url = reverse_lazy('horizon:project:cgroups:index')
submit_url = "horizon:project:cgroups:update"
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 RemoveVolumesView(forms.ModalFormView):
template_name = 'project/cgroups/remove_vols.html'
page_title = _("Remove Volumes from Consistency Group")
form_class = vol_cgroup_forms.RemoveVolsForm
success_url = reverse_lazy('horizon:project:cgroups:index')
submit_url = "horizon:project:cgroups:remove_volumes"
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/cgroups/delete.html'
page_title = _("Delete Consistency Group")
form_class = vol_cgroup_forms.DeleteForm
success_url = reverse_lazy('horizon:project:cgroups:index')
submit_url = "horizon:project:cgroups:delete"
submit_label = page_title
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
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 CreateSnapshotView(forms.ModalFormView):
form_class = vol_cgroup_forms.CreateSnapshotForm
page_title = _("Create Consistency Group Snapshot")
template_name = 'project/cgroups/create_snapshot.html'
submit_label = _("Create Snapshot")
submit_url = "horizon:project:cgroups:create_snapshot"
success_url = reverse_lazy('horizon:project:cg_snapshots:index')
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_quota_usages(
self.request, targets=('snapshots', 'gigabytes'))
if (usages['snapshots']['used'] + num_volumes >
usages['snapshots']['quota']):
raise ValueError(_('Unable to create snapshots due to '
'exceeding snapshot quota limit.'))
else:
context['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
page_title = _("Clone Consistency Group")
template_name = 'project/cgroups/clone_cgroup.html'
submit_label = _("Clone Consistency Group")
submit_url = "horizon:project:cgroups:clone_cgroup"
success_url = reverse_lazy('horizon:project:cgroups:index')
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_quota_usages(
self.request, targets=('volumes', 'gigabytes'))
if (usages['volumes']['used'] + num_volumes >
usages['volumes']['quota']):
raise ValueError(_('Unable to create consistency group due to '
'exceeding volume quota limit.'))
else:
context['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'
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:cgroups:index')
def get_tabs(self, request, *args, **kwargs):
cgroup = self.get_data()
return self.tab_group_class(request, cgroup=cgroup, **kwargs)

View File

@ -1,414 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.api import cinder
INDEX_URL = "horizon:project:cgroups: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.ThemableSelectWidget(
attrs={'class': 'switched',
'data-switch-on': 'source',
'data-source-no_source_type': _('Availability Zone'),
'data-source-image_source': _('Availability Zone')}))
def __init__(self, request, *args, **kwargs):
super(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():
# ensure new name has reasonable length
formatted_name = name
if len(name) > 20:
formatted_name = name[:14] + "..." + name[-3:]
raise forms.ValidationError(
_('The name "%s" is already used by '
'another consistency group.')
% formatted_name
)
return cleaned_data
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_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
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:
cgroup_id = None
vol_is_available = False
in_this_cgroup = False
if hasattr(volume, 'consistencygroup_id'):
# this vol already belongs to a CG.
# only include it here if it belongs to this CG
cgroup_id = volume.consistencygroup_id
if not cgroup_id:
# put this vol in the available list
vol_is_available = True
elif cgroup_id == self.initial['cgroup_id']:
# put this vol in the assigned to CG list
vol_is_available = True
in_this_cgroup = True
if vol_is_available:
vol_list.append({'volume_name': volume.name,
'volume_id': volume.id,
'in_cgroup': in_this_cgroup,
'is_duplicate': False})
sorted_vol_list = sorted(vol_list, key=lambda k: k['volume_name'])
# mark any duplicate volume names
for index, volume in enumerate(sorted_vol_list):
if index < len(sorted_vol_list) - 1:
if volume['volume_name'] == \
sorted_vol_list[index + 1]['volume_name']:
volume['is_duplicate'] = True
sorted_vol_list[index + 1]['is_duplicate'] = True
# update display with all available vols and those already
# assigned to consistency group
available_vols = []
assigned_vols = []
for volume in sorted_vol_list:
if volume['is_duplicate']:
# add id to differentiate volumes to user
entry = volume['volume_name'] + \
" [" + volume['volume_id'] + "]"
else:
entry = volume['volume_name']
available_vols.append((volume['volume_id'], entry))
if volume['in_cgroup']:
assigned_vols.append(volume['volume_id'])
except Exception:
exceptions.handle(request, err_msg)
self.fields[field_name].choices = \
available_vols
self.fields[field_name].initial = assigned_vols
class Meta(object):
name = _("Manage Volumes")
slug = "add_volumes_to_cgroup"
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 invalid_backend:
continue
for vol_type in vol_types:
if selected_vol_type != vol_type.id:
continue
if (hasattr(vol_type, "extra_specs") and
'volume_backend_name' in vol_type.extra_specs):
vol_type_backend = \
vol_type.extra_specs['volume_backend_name']
if vol_type_backend is None:
invalid_backend = True
break
if backend_name is None:
backend_name = vol_type_backend
if vol_type_backend != backend_name:
invalid_backend = True
break
else:
invalid_backend = True
break
if invalid_backend:
msg = _('All selected volume types must be associated '
'with the same volume backend name.')
exceptions.handle(request, msg)
return False
try:
vtypes_str = ",".join(context['volume_types'])
self.object = \
cinder.volume_cgroup_create(
request,
vtypes_str,
context['name'],
description=context['description'],
availability_zone=context['availability_zone'])
except Exception:
exceptions.handle(request, _('Unable to create consistency '
'group.'))
return False
return True
class UpdateCGroupWorkflow(workflows.Workflow):
slug = "update_cgroup"
name = _("Add/Remove Consistency Group Volumes")
finalize_button_name = _("Submit")
success_message = _('Updated volumes for consistency group "%s".')
failure_message = _('Unable to update volumes for consistency group')
success_url = INDEX_URL
default_steps = (AddVolumesToCGroupStep,)
def handle(self, request, context):
cgroup_id = context['cgroup_id']
add_vols = []
remove_vols = []
try:
selected_volumes = context['volumes']
volumes = cinder.volume_list(request)
# scan all volumes and make correct consistency group is set
for volume in volumes:
selected = False
for selection in selected_volumes:
if selection == volume.id:
selected = True
break
if selected:
# ensure this volume is in this consistency group
if hasattr(volume, 'consistencygroup_id'):
if volume.consistencygroup_id != cgroup_id:
add_vols.append(volume.id)
else:
add_vols.append(volume.id)
else:
# ensure this volume is not in our consistency group
if hasattr(volume, 'consistencygroup_id'):
if volume.consistencygroup_id == cgroup_id:
# remove from this CG
remove_vols.append(volume.id)
add_vols_str = ",".join(add_vols)
remove_vols_str = ",".join(remove_vols)
if not add_vols_str and not remove_vols_str:
# nothing to change
return True
cinder.volume_cgroup_update(request,
cgroup_id,
name=context['name'],
add_vols=add_vols_str,
remove_vols=remove_vols_str)
except Exception:
# error message supplied by form
return False
return True

View File

@ -47,7 +47,6 @@ class CreateGroupForm(forms.SelfHandlingForm):
def __init__(self, request, *args, **kwargs):
super(CreateGroupForm, self).__init__(request, *args, **kwargs)
# populate cgroup_id
vg_snapshot_id = kwargs.get('initial', {}).get('vg_snapshot_id', [])
self.fields['vg_snapshot_id'] = forms.CharField(
widget=forms.HiddenInput(),

View File

@ -152,7 +152,7 @@ class VolumeGroupTests(test.TestCase):
@test.create_mocks({cinder: ['group_get', 'group_delete']})
def test_delete_group_delete_volumes_flag(self):
group = self.cinder_consistencygroups.first()
group = self.cinder_groups.first()
formData = {'delete_volumes': True}
self.mock_group_get.return_value = group

View File

@ -1,9 +0,0 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'cgroups'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'volumes'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.project.cgroups.panel.CGroups'

View File

@ -1,10 +0,0 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'cg_snapshots'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'volumes'
# Python panel class of the PANEL to be added.
ADD_PANEL = \
'openstack_dashboard.dashboards.project.cg_snapshots.panel.CGSnapshots'

View File

@ -289,7 +289,6 @@ POLICY_FILES = {
# in POLICY_DIRS by default.
POLICY_DIRS = {
'compute': ['nova_policy.d'],
'volume': ['cinder_policy.d'],
}
SECRET_KEY = None

View File

@ -275,16 +275,6 @@ TEST_GLOBAL_MOCKS_ON_PANELS = {
'.aggregates.panel.Aggregates.can_access'),
'return_value': True,
},
'cgroups': {
'method': ('openstack_dashboard.dashboards.project'
'.cgroups.panel.CGroups.allowed'),
'return_value': True,
},
'cg_snapshots': {
'method': ('openstack_dashboard.dashboards.project'
'.cg_snapshots.panel.CGSnapshots.allowed'),
'return_value': True,
},
'domains': {
'method': ('openstack_dashboard.dashboards.identity'
'.domains.panel.Domains.can_access'),

View File

@ -13,8 +13,6 @@
# under the License.
from cinderclient.v2 import availability_zones
from cinderclient.v2 import cgsnapshots
from cinderclient.v2 import consistencygroups
from cinderclient.v2.contrib import list_extensions as cinder_list_extensions
from cinderclient.v2 import pools
from cinderclient.v2 import qos_specs
@ -55,9 +53,6 @@ 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()
TEST.cinder_cg_snapshots = utils.TestDataContainer()
TEST.cinder_groups = utils.TestDataContainer()
TEST.cinder_group_types = utils.TestDataContainer()
TEST.cinder_group_snapshots = utils.TestDataContainer()
@ -456,49 +451,6 @@ 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': ['1'],
'volume_type_names': []})
cgroup_2 = consistencygroups.Consistencygroup(
consistencygroups.ConsistencygroupManager(None),
{'id': u'2',
'name': u'cg_2',
'description': 'cg 2 description',
'volume_types': ['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,
'name': 'Volume name',
'display_description': 'Volume description',
'created_at': '2014-01-27 10:30:00',
'volume_type': 'vol_type_1',
'attachments': [],
'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)
group_type_1 = group_types.GroupType(
group_types.GroupTypeManager(None),
{

View File

@ -446,85 +446,6 @@ class CinderApiTests(test.APIMockTestCase):
self.assertEqual(default_volume_type, volume_type)
cinderclient.volume_types.default.assert_called_once()
@mock.patch.object(api.cinder, 'cinderclient')
def test_cgroup_list(self, mock_cinderclient):
cgroups = self.cinder_consistencygroups.list()
cinderclient = mock_cinderclient.return_value
mock_cgs = cinderclient.consistencygroups.list
mock_cgs.return_value = cgroups
api_cgroups = api.cinder.volume_cgroup_list(self.request)
self.assertEqual(len(cgroups), len(api_cgroups))
mock_cgs.assert_called_once_with(search_opts=None)
@mock.patch.object(api.cinder, 'cinderclient')
def test_cgroup_get(self, mock_cinderclient):
cgroup = self.cinder_consistencygroups.first()
cinderclient = mock_cinderclient.return_value
mock_cg = cinderclient.consistencygroups.get
mock_cg.return_value = cgroup
api_cgroup = api.cinder.volume_cgroup_get(self.request, cgroup.id)
mock_cg.assert_called_once_with(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)
@mock.patch.object(api.cinder, 'cinderclient')
def test_cgroup_list_with_vol_type_names(self, mock_cinderclient):
cgroups = self.cinder_consistencygroups.list()
volume_types_list = self.cinder_volume_types.list()
cinderclient = mock_cinderclient.return_value
mock_cgs = cinderclient.consistencygroups.list
mock_cgs.return_value = cgroups
mock_volume_types = cinderclient.volume_types.list
mock_volume_types.return_value = volume_types_list
api_cgroups = api.cinder.volume_cgroup_list_with_vol_type_names(
self.request)
mock_cgs.assert_called_once_with(search_opts=None)
mock_volume_types.assert_called_once()
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])
@mock.patch.object(api.cinder, 'cinderclient')
def test_cgsnapshot_list(self, mock_cinderclient):
cgsnapshots = self.cinder_cg_snapshots.list()
cinderclient = mock_cinderclient.return_value
mock_cg_snapshots = cinderclient.cgsnapshots.list
mock_cg_snapshots.return_value = cgsnapshots
api_cgsnapshots = api.cinder.volume_cg_snapshot_list(self.request)
mock_cg_snapshots.assert_called_once_with(search_opts=None)
self.assertEqual(len(cgsnapshots), len(api_cgsnapshots))
@mock.patch.object(api.cinder, 'cinderclient')
def test_cgsnapshot_get(self, mock_cinderclient):
cgsnapshot = self.cinder_cg_snapshots.first()
cinderclient = mock_cinderclient.return_value
mock_cg_snapshot = cinderclient.cgsnapshots.get
mock_cg_snapshot.return_value = cgsnapshot
api_cgsnapshot = api.cinder.volume_cg_snapshot_get(self.request,
cgsnapshot.id)
mock_cg_snapshot.assert_called_once_with(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):

View File

@ -0,0 +1,8 @@
---
upgrade:
- |
Cinder consistency group support in horizon has been dropped
in Train release. It was deprecated in Pike release in Cinder
and deprecated in Stein release in Horizon.
The feature is superseded by the generic group feature and
horizon provides full support of the generic group.