Add possibility to revert a share to a snapshot
Since 2.27 API microversion we are able to revert shares to snapshots. So, add its support. Change-Id: I91ac5071d40588584558c3bd6458f335c6cbc25a Implements BluePrint add-revert-share-to-snapshot-support
This commit is contained in:
parent
eeb254081c
commit
e19703361b
@ -175,6 +175,17 @@ def share_extend(request, share_id, new_size):
|
|||||||
return manilaclient(request).shares.extend(share_id, new_size)
|
return manilaclient(request).shares.extend(share_id, new_size)
|
||||||
|
|
||||||
|
|
||||||
|
def share_revert(request, share, snapshot):
|
||||||
|
"""Sends request to revert share to specific snapshot.
|
||||||
|
|
||||||
|
This API available only since 2.27 microversion.
|
||||||
|
|
||||||
|
:param share: Share class instance or share ID
|
||||||
|
:param snapshot: ShareSnapshot class instance or share snapshot ID
|
||||||
|
"""
|
||||||
|
return manilaclient(request).shares.revert_to_snapshot(share, snapshot)
|
||||||
|
|
||||||
|
|
||||||
def share_snapshot_get(request, snapshot_id):
|
def share_snapshot_get(request, snapshot_id):
|
||||||
return manilaclient(request).share_snapshots.get(snapshot_id)
|
return manilaclient(request).share_snapshots.get(snapshot_id)
|
||||||
|
|
||||||
|
@ -405,3 +405,48 @@ class ExtendForm(forms.SelfHandlingForm):
|
|||||||
exceptions.handle(request,
|
exceptions.handle(request,
|
||||||
_('Unable to extend share.'),
|
_('Unable to extend share.'),
|
||||||
redirect=redirect)
|
redirect=redirect)
|
||||||
|
|
||||||
|
|
||||||
|
class RevertForm(forms.SelfHandlingForm):
|
||||||
|
"""Form for reverting a share to a snapshot."""
|
||||||
|
|
||||||
|
snapshot = forms.ChoiceField(
|
||||||
|
label=_("Snapshot"),
|
||||||
|
required=True,
|
||||||
|
widget=forms.Select(
|
||||||
|
attrs={'class': 'switchable', 'data-slug': 'share_snapshot'}))
|
||||||
|
|
||||||
|
def __init__(self, req, *args, **kwargs):
|
||||||
|
super(self.__class__, self).__init__(req, *args, **kwargs)
|
||||||
|
# NOTE(vponomaryov): manila client does not allow to filter snapshots
|
||||||
|
# using "created_at" field, so, we need to get all snapshots of a share
|
||||||
|
# and do filtering here.
|
||||||
|
search_opts = {'share_id': self.initial['share_id']}
|
||||||
|
snapshots = manila.share_snapshot_list(req, search_opts=search_opts)
|
||||||
|
amount_of_snapshots = len(snapshots)
|
||||||
|
if amount_of_snapshots < 1:
|
||||||
|
self.fields['snapshot'].choices = [("", "")]
|
||||||
|
else:
|
||||||
|
snapshot = snapshots[0]
|
||||||
|
if amount_of_snapshots > 1:
|
||||||
|
for s in snapshots[1:]:
|
||||||
|
if s.created_at > snapshot.created_at:
|
||||||
|
snapshot = s
|
||||||
|
self.fields['snapshot'].choices = [
|
||||||
|
(snapshot.id, snapshot.name or snapshot.id)]
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
share_id = self.initial['share_id']
|
||||||
|
snapshot_id = data['snapshot']
|
||||||
|
try:
|
||||||
|
manila.share_revert(request, share_id, snapshot_id)
|
||||||
|
message = _('Share "%(s)s" has been reverted to "%(ss)s" snapshot '
|
||||||
|
'successfully') % {'s': share_id, 'ss': snapshot_id}
|
||||||
|
messages.success(request, message)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
redirect = reverse("horizon:project:shares:index")
|
||||||
|
exceptions.handle(
|
||||||
|
request,
|
||||||
|
_('Unable to revert share to the snapshot.'),
|
||||||
|
redirect=redirect)
|
||||||
|
@ -135,6 +135,26 @@ class ExtendShare(tables.LinkAction):
|
|||||||
return share.status.lower() in ("available",)
|
return share.status.lower() in ("available",)
|
||||||
|
|
||||||
|
|
||||||
|
class RevertShare(tables.LinkAction):
|
||||||
|
name = "revert_share"
|
||||||
|
verbose_name = _("Revert Share")
|
||||||
|
url = "horizon:project:shares:revert"
|
||||||
|
classes = ("ajax-modal", "btn-create")
|
||||||
|
policy_rules = (("share", "share:revert"),)
|
||||||
|
|
||||||
|
def get_policy_target(self, request, datum=None):
|
||||||
|
project_id = None
|
||||||
|
if datum:
|
||||||
|
project_id = getattr(datum, "os-share-tenant-attr:tenant_id", None)
|
||||||
|
return {"project_id": project_id}
|
||||||
|
|
||||||
|
def allowed(self, request, share=None):
|
||||||
|
return (
|
||||||
|
share.revert_to_snapshot_support and
|
||||||
|
share.status.lower() == "available"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UpdateRow(tables.Row):
|
class UpdateRow(tables.Row):
|
||||||
ajax = True
|
ajax = True
|
||||||
|
|
||||||
@ -162,12 +182,14 @@ class SharesTableBase(tables.DataTable):
|
|||||||
("available", True), ("AVAILABLE", True),
|
("available", True), ("AVAILABLE", True),
|
||||||
("creating", None), ("CREATING", None),
|
("creating", None), ("CREATING", None),
|
||||||
("deleting", None), ("DELETING", None),
|
("deleting", None), ("DELETING", None),
|
||||||
|
("reverting", None),
|
||||||
("migrating", None), ("migrating_to", None),
|
("migrating", None), ("migrating_to", None),
|
||||||
("error", False), ("ERROR", False),
|
("error", False), ("ERROR", False),
|
||||||
("error_deleting", False), ("ERROR_DELETING", False),
|
("error_deleting", False), ("ERROR_DELETING", False),
|
||||||
("MANAGE_ERROR", False),
|
("MANAGE_ERROR", False),
|
||||||
("UNMANAGE_ERROR", False),
|
("UNMANAGE_ERROR", False),
|
||||||
("extending_error", False),
|
("extending_error", False),
|
||||||
|
("reverting_error", False),
|
||||||
)
|
)
|
||||||
STATUS_DISPLAY_CHOICES = (
|
STATUS_DISPLAY_CHOICES = (
|
||||||
("available", pgettext_lazy("Current status of share", u"Available")),
|
("available", pgettext_lazy("Current status of share", u"Available")),
|
||||||
@ -191,6 +213,8 @@ class SharesTableBase(tables.DataTable):
|
|||||||
u"Unmanage Error")),
|
u"Unmanage Error")),
|
||||||
("extending_error", pgettext_lazy("Current status of share",
|
("extending_error", pgettext_lazy("Current status of share",
|
||||||
u"Extending Error")),
|
u"Extending Error")),
|
||||||
|
("reverting_error", pgettext_lazy("Current status of share",
|
||||||
|
u"Reverting Error")),
|
||||||
)
|
)
|
||||||
name = tables.WrappingColumn(
|
name = tables.WrappingColumn(
|
||||||
"name", verbose_name=_("Name"),
|
"name", verbose_name=_("Name"),
|
||||||
@ -331,6 +355,7 @@ class SharesTable(SharesTableBase):
|
|||||||
row_actions = (
|
row_actions = (
|
||||||
EditShare,
|
EditShare,
|
||||||
ExtendShare,
|
ExtendShare,
|
||||||
|
RevertShare,
|
||||||
snapshot_tables.CreateSnapshot,
|
snapshot_tables.CreateSnapshot,
|
||||||
ManageRules,
|
ManageRules,
|
||||||
ManageReplicas,
|
ManageReplicas,
|
||||||
|
@ -283,3 +283,37 @@ class ExtendView(forms.ModalFormView):
|
|||||||
'orig_size': share.size,
|
'orig_size': share.size,
|
||||||
'new_size': int(share.size) + 1,
|
'new_size': int(share.size) + 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RevertView(forms.ModalFormView):
|
||||||
|
form_class = share_form.RevertForm
|
||||||
|
form_id = "revert_share"
|
||||||
|
template_name = 'project/shares/shares/revert.html'
|
||||||
|
modal_header = _("Revert Share to a Snapshot")
|
||||||
|
modal_id = "revert_share_modal"
|
||||||
|
submit_label = _("Revert share to a snapshot")
|
||||||
|
submit_url = "horizon:project:shares:revert"
|
||||||
|
success_url = reverse_lazy("horizon:project:shares:index")
|
||||||
|
page_title = _('Revert Share to a Snapshot')
|
||||||
|
|
||||||
|
@memoized.memoized_method
|
||||||
|
def get_object(self):
|
||||||
|
try:
|
||||||
|
return manila.share_get(self.request, self.kwargs['share_id'])
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request, _('Unable to retrieve share.'))
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(self.__class__, self).get_context_data(**kwargs)
|
||||||
|
args = (self.get_object().id,)
|
||||||
|
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
share = self.get_object()
|
||||||
|
if not share or isinstance(share, Exception):
|
||||||
|
raise exceptions.NotFound()
|
||||||
|
return {
|
||||||
|
'share_id': self.kwargs["share_id"],
|
||||||
|
'name': share.name or share.id,
|
||||||
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block modal-body-right %}
|
||||||
|
<h3>{% trans "Description" %}:</h3>
|
||||||
|
<p>{% trans "From here you can revert the share to its latest snapshot." %}</p>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Revert Share" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include 'project/shares/shares/_revert.html' %}
|
||||||
|
{% endblock %}
|
@ -79,6 +79,9 @@ urlpatterns = [
|
|||||||
url(r'^(?P<share_id>[^/]+)/extend/$',
|
url(r'^(?P<share_id>[^/]+)/extend/$',
|
||||||
shares_views.ExtendView.as_view(),
|
shares_views.ExtendView.as_view(),
|
||||||
name='extend'),
|
name='extend'),
|
||||||
|
url(r'^(?P<share_id>[^/]+)/revert/$',
|
||||||
|
shares_views.RevertView.as_view(),
|
||||||
|
name='revert'),
|
||||||
url(r'^(?P<snapshot_id>[^/]+)/snapshot_rules/$',
|
url(r'^(?P<snapshot_id>[^/]+)/snapshot_rules/$',
|
||||||
snapshot_views.ManageRulesView.as_view(),
|
snapshot_views.ManageRulesView.as_view(),
|
||||||
name='snapshot_manage_rules'),
|
name='snapshot_manage_rules'),
|
||||||
|
@ -72,6 +72,15 @@ class ManilaApiTests(base.APITestCase):
|
|||||||
self.id, new_size
|
self.id, new_size
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_share_revert(self):
|
||||||
|
share = 'fake_share'
|
||||||
|
snapshot = 'fake_snapshot'
|
||||||
|
|
||||||
|
api.share_revert(self.request, share, snapshot)
|
||||||
|
|
||||||
|
self.manilaclient.shares.revert_to_snapshot.assert_called_once_with(
|
||||||
|
share, snapshot)
|
||||||
|
|
||||||
@ddt.data(True, False)
|
@ddt.data(True, False)
|
||||||
def test_share_type_create_with_default_values(self, dhss):
|
def test_share_type_create_with_default_values(self, dhss):
|
||||||
name = 'fake_share_type_name'
|
name = 'fake_share_type_name'
|
||||||
|
@ -45,7 +45,7 @@ class QuotaTests(test.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
class ShareViewTests(test.TestCase):
|
class ShareViewTests(test.APITestCase):
|
||||||
|
|
||||||
class FakeAZ(object):
|
class FakeAZ(object):
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
@ -61,6 +61,11 @@ class ShareViewTests(test.TestCase):
|
|||||||
self.share = test_data.share
|
self.share = test_data.share
|
||||||
self.mock_object(
|
self.mock_object(
|
||||||
api_manila, "share_get", mock.Mock(return_value=self.share))
|
api_manila, "share_get", mock.Mock(return_value=self.share))
|
||||||
|
self.mock_object(
|
||||||
|
neutron, "is_service_enabled", mock.Mock(return_value=[True]))
|
||||||
|
self.mock_object(horizon_messages, "success")
|
||||||
|
FAKE_ENVIRON = {'REQUEST_METHOD': 'GET', 'wsgi.input': 'fake_input'}
|
||||||
|
self.request = wsgi.WSGIRequest(FAKE_ENVIRON)
|
||||||
|
|
||||||
@mock.patch.object(api_manila, 'availability_zone_list')
|
@mock.patch.object(api_manila, 'availability_zone_list')
|
||||||
def test_create_share(self, az_list):
|
def test_create_share(self, az_list):
|
||||||
@ -455,6 +460,102 @@ class ShareViewTests(test.TestCase):
|
|||||||
quotas.tenant_limit_usages.assert_called_once_with(mock.ANY)
|
quotas.tenant_limit_usages.assert_called_once_with(mock.ANY)
|
||||||
self.assertRedirectsNoFollow(response, SHARE_INDEX_URL)
|
self.assertRedirectsNoFollow(response, SHARE_INDEX_URL)
|
||||||
|
|
||||||
|
def test_revert_to_snapshot_get_success(self):
|
||||||
|
snapshots = [
|
||||||
|
type('FakeSnapshot', (object, ),
|
||||||
|
{'name': s_n, 'id': s_id, 'created_at': c_at})
|
||||||
|
for s_n, s_id, c_at in (
|
||||||
|
('foo_name', 'foo_id', '2017-04-20T12:31:14.000000'),
|
||||||
|
('bar_name', 'bar_id', '2017-04-20T12:31:16.000000'))
|
||||||
|
]
|
||||||
|
url = reverse('horizon:project:shares:revert', args=[self.share.id])
|
||||||
|
self.mock_object(api_manila, "share_revert")
|
||||||
|
self.mock_object(
|
||||||
|
api_manila, "share_snapshot_list",
|
||||||
|
mock.Mock(return_value=snapshots))
|
||||||
|
|
||||||
|
res = self.client.get(url)
|
||||||
|
|
||||||
|
api_manila.share_get.assert_called_once_with(mock.ANY, self.share.id)
|
||||||
|
api_manila.share_snapshot_list.assert_called_once_with(
|
||||||
|
mock.ANY, search_opts={'share_id': self.share.id})
|
||||||
|
api_manila.share_revert.assert_not_called()
|
||||||
|
self.assertNoMessages()
|
||||||
|
self.assertTemplateUsed(res, 'project/shares/shares/revert.html')
|
||||||
|
|
||||||
|
def test_revert_to_snapshot_post_success(self):
|
||||||
|
snapshots = [
|
||||||
|
type('FakeSnapshot', (object, ),
|
||||||
|
{'name': s_n, 'id': s_id, 'created_at': c_at})
|
||||||
|
for s_n, s_id, c_at in (
|
||||||
|
('foo_name', 'foo_id', '2017-04-20T12:31:14.000000'),
|
||||||
|
('bar_name', 'bar_id', '2017-04-20T12:31:16.000000'),
|
||||||
|
('quuz_name', 'quuz_id', '2017-04-20T12:31:13.000000'))
|
||||||
|
]
|
||||||
|
url = reverse('horizon:project:shares:revert', args=[self.share.id])
|
||||||
|
self.mock_object(api_manila, "share_revert")
|
||||||
|
self.mock_object(
|
||||||
|
api_manila, "share_snapshot_list",
|
||||||
|
mock.Mock(return_value=snapshots))
|
||||||
|
data = {'snapshot': snapshots[1].id}
|
||||||
|
|
||||||
|
res = self.client.post(url, data)
|
||||||
|
|
||||||
|
api_manila.share_get.assert_called_once_with(mock.ANY, self.share.id)
|
||||||
|
api_manila.share_snapshot_list.assert_called_once_with(
|
||||||
|
mock.ANY, search_opts={'share_id': self.share.id})
|
||||||
|
api_manila.share_revert.assert_called_once_with(
|
||||||
|
mock.ANY, self.share.id, data['snapshot'])
|
||||||
|
self.assertNoMessages()
|
||||||
|
self.assertTemplateNotUsed(res, 'project/shares/shares/revert.html')
|
||||||
|
self.assertRedirectsNoFollow(res, SHARE_INDEX_URL)
|
||||||
|
|
||||||
|
def test_revert_to_snapshot_share_not_found(self):
|
||||||
|
url = reverse("horizon:project:shares:revert", args=[self.share.id])
|
||||||
|
self.mock_object(api_manila, "share_revert")
|
||||||
|
api_manila.share_get.side_effect = Exception(
|
||||||
|
'Fake share NotFound exception')
|
||||||
|
self.mock_object(
|
||||||
|
api_manila, "share_snapshot_list", mock.Mock(return_value=[]))
|
||||||
|
|
||||||
|
res = self.client.get(url)
|
||||||
|
|
||||||
|
self.assertEqual(404, res.status_code)
|
||||||
|
self.assertTemplateNotUsed(
|
||||||
|
res, 'project/shares/shares/revert.html')
|
||||||
|
api_manila.share_revert.assert_not_called()
|
||||||
|
api_manila.share_snapshot_list.assert_not_called()
|
||||||
|
api_manila.share_get.assert_called_once_with(mock.ANY, self.share.id)
|
||||||
|
|
||||||
|
def test_revert_to_snapshot_failed(self):
|
||||||
|
snapshots = [
|
||||||
|
type('FakeSnapshot', (object, ),
|
||||||
|
{'name': s_n, 'id': s_id, 'created_at': c_at})
|
||||||
|
for s_n, s_id, c_at in (
|
||||||
|
('foo_name', 'foo_id', '2017-04-20T12:31:14.000000'),
|
||||||
|
('bar_name', 'bar_id', '2017-04-20T12:31:16.000000'),
|
||||||
|
('quuz_name', 'quuz_id', '2017-04-20T12:31:13.000000'))
|
||||||
|
]
|
||||||
|
url = reverse('horizon:project:shares:revert', args=[self.share.id])
|
||||||
|
self.mock_object(
|
||||||
|
api_manila, "share_revert",
|
||||||
|
mock.Mock(side_effect=Exception('Fake reverting error')))
|
||||||
|
self.mock_object(
|
||||||
|
api_manila, "share_snapshot_list",
|
||||||
|
mock.Mock(return_value=snapshots))
|
||||||
|
data = {'snapshot': snapshots[1].id}
|
||||||
|
|
||||||
|
res = self.client.post(url, data)
|
||||||
|
|
||||||
|
self.assertEqual(302, res.status_code)
|
||||||
|
api_manila.share_get.assert_called_once_with(mock.ANY, self.share.id)
|
||||||
|
api_manila.share_snapshot_list.assert_called_once_with(
|
||||||
|
mock.ANY, search_opts={'share_id': self.share.id})
|
||||||
|
api_manila.share_revert.assert_called_once_with(
|
||||||
|
mock.ANY, self.share.id, data['snapshot'])
|
||||||
|
self.assertTemplateNotUsed(res, 'project/shares/shares/revert.html')
|
||||||
|
self.assertRedirectsNoFollow(res, SHARE_INDEX_URL)
|
||||||
|
|
||||||
def test_update_share_metadata_get(self):
|
def test_update_share_metadata_get(self):
|
||||||
share = test_data.share_with_metadata
|
share = test_data.share_with_metadata
|
||||||
url = reverse(
|
url = reverse(
|
||||||
@ -493,18 +594,6 @@ class ShareViewTests(test.TestCase):
|
|||||||
mock.ANY, share, form_data['metadata'])
|
mock.ANY, share, form_data['metadata'])
|
||||||
self.assertRedirectsNoFollow(res, SHARE_INDEX_URL)
|
self.assertRedirectsNoFollow(res, SHARE_INDEX_URL)
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
|
||||||
class ShareViewFormTests(ShareViewTests, test.APITestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(ShareViewFormTests, self).setUp()
|
|
||||||
FAKE_ENVIRON = {'REQUEST_METHOD': 'GET', 'wsgi.input': 'fake_input'}
|
|
||||||
self.request = wsgi.WSGIRequest(FAKE_ENVIRON)
|
|
||||||
self.mock_object(
|
|
||||||
horizon_messages, "success",
|
|
||||||
mock.Mock())
|
|
||||||
|
|
||||||
@ddt.data((True, True), (True, False), (False, False))
|
@ddt.data((True, True), (True, False), (False, False))
|
||||||
@ddt.unpack
|
@ddt.unpack
|
||||||
def test_enable_public_share_creation(self,
|
def test_enable_public_share_creation(self,
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added possibility to revert share to its latest snapshot.
|
Loading…
Reference in New Issue
Block a user