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:
parent
4ab7f806b8
commit
431fd6c16b
@ -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."""
|
||||
|
@ -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"],
|
||||
}
|
||||
|
@ -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": ""
|
@ -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
|
||||
|
@ -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)
|
@ -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
|
@ -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')
|
||||
]
|
@ -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,)
|
@ -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 %}
|
@ -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>
|
@ -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 %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/cg_snapshots/_create.html' %}
|
||||
{% endblock %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/cg_snapshots/_update.html' %}
|
||||
{% endblock %}
|
@ -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)
|
@ -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'),
|
||||
]
|
@ -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"]}
|
@ -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)
|
@ -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
|
@ -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']
|
@ -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,)
|
@ -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 %}
|
@ -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 %}
|
@ -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 "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 %}
|
@ -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>
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/cgroups/_clone_cgroup.html' %}
|
||||
{% endblock %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'horizon/common/_workflow.html' %}
|
||||
{% endblock %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/cgroups/_create_snapshot.html' %}
|
||||
{% endblock %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/cgroups/_delete.html' %}
|
||||
{% endblock %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/cgroups/_remove_vols.html' %}
|
||||
{% endblock %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/cgroups/_update.html' %}
|
||||
{% endblock %}
|
@ -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'])
|
@ -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'),
|
||||
]
|
@ -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)
|
@ -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
|
@ -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(),
|
||||
|
@ -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
|
||||
|
@ -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'
|
@ -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'
|
@ -289,7 +289,6 @@ POLICY_FILES = {
|
||||
# in POLICY_DIRS by default.
|
||||
POLICY_DIRS = {
|
||||
'compute': ['nova_policy.d'],
|
||||
'volume': ['cinder_policy.d'],
|
||||
}
|
||||
|
||||
SECRET_KEY = None
|
||||
|
@ -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'),
|
||||
|
@ -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),
|
||||
{
|
||||
|
@ -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):
|
||||
|
||||
|
8
releasenotes/notes/drop-cgroup-c1f6b169ba10a5d3.yaml
Normal file
8
releasenotes/notes/drop-cgroup-c1f6b169ba10a5d3.yaml
Normal 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.
|
Loading…
Reference in New Issue
Block a user