Merge "Drop cinder consistency group support"
This commit is contained in:
commit
27d7b697ee
|
@ -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):
|
||||
|
||||
|
|
|
@ -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