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:
Valeriy Ponomaryov 2017-04-21 14:55:00 +03:00
parent eeb254081c
commit e19703361b
10 changed files with 245 additions and 13 deletions

View File

@ -175,6 +175,17 @@ def share_extend(request, 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):
return manilaclient(request).share_snapshots.get(snapshot_id)

View File

@ -405,3 +405,48 @@ class ExtendForm(forms.SelfHandlingForm):
exceptions.handle(request,
_('Unable to extend share.'),
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)

View File

@ -135,6 +135,26 @@ class ExtendShare(tables.LinkAction):
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):
ajax = True
@ -162,12 +182,14 @@ class SharesTableBase(tables.DataTable):
("available", True), ("AVAILABLE", True),
("creating", None), ("CREATING", None),
("deleting", None), ("DELETING", None),
("reverting", None),
("migrating", None), ("migrating_to", None),
("error", False), ("ERROR", False),
("error_deleting", False), ("ERROR_DELETING", False),
("MANAGE_ERROR", False),
("UNMANAGE_ERROR", False),
("extending_error", False),
("reverting_error", False),
)
STATUS_DISPLAY_CHOICES = (
("available", pgettext_lazy("Current status of share", u"Available")),
@ -191,6 +213,8 @@ class SharesTableBase(tables.DataTable):
u"Unmanage Error")),
("extending_error", pgettext_lazy("Current status of share",
u"Extending Error")),
("reverting_error", pgettext_lazy("Current status of share",
u"Reverting Error")),
)
name = tables.WrappingColumn(
"name", verbose_name=_("Name"),
@ -331,6 +355,7 @@ class SharesTable(SharesTableBase):
row_actions = (
EditShare,
ExtendShare,
RevertShare,
snapshot_tables.CreateSnapshot,
ManageRules,
ManageReplicas,

View File

@ -283,3 +283,37 @@ class ExtendView(forms.ModalFormView):
'orig_size': share.size,
'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,
}

View File

@ -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 %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Revert Share" %}{% endblock %}
{% block main %}
{% include 'project/shares/shares/_revert.html' %}
{% endblock %}

View File

@ -79,6 +79,9 @@ urlpatterns = [
url(r'^(?P<share_id>[^/]+)/extend/$',
shares_views.ExtendView.as_view(),
name='extend'),
url(r'^(?P<share_id>[^/]+)/revert/$',
shares_views.RevertView.as_view(),
name='revert'),
url(r'^(?P<snapshot_id>[^/]+)/snapshot_rules/$',
snapshot_views.ManageRulesView.as_view(),
name='snapshot_manage_rules'),

View File

@ -72,6 +72,15 @@ class ManilaApiTests(base.APITestCase):
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)
def test_share_type_create_with_default_values(self, dhss):
name = 'fake_share_type_name'

View File

@ -45,7 +45,7 @@ class QuotaTests(test.TestCase):
@ddt.ddt
class ShareViewTests(test.TestCase):
class ShareViewTests(test.APITestCase):
class FakeAZ(object):
def __init__(self, name):
@ -61,6 +61,11 @@ class ShareViewTests(test.TestCase):
self.share = test_data.share
self.mock_object(
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')
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)
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):
share = test_data.share_with_metadata
url = reverse(
@ -493,18 +594,6 @@ class ShareViewTests(test.TestCase):
mock.ANY, share, form_data['metadata'])
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.unpack
def test_enable_public_share_creation(self,

View File

@ -0,0 +1,3 @@
---
features:
- Added possibility to revert share to its latest snapshot.