diff --git a/manila_ui/api/manila.py b/manila_ui/api/manila.py index af693d60..8350f784 100644 --- a/manila_ui/api/manila.py +++ b/manila_ui/api/manila.py @@ -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) diff --git a/manila_ui/dashboards/project/shares/shares/forms.py b/manila_ui/dashboards/project/shares/shares/forms.py index cf8b847e..cebcaab4 100644 --- a/manila_ui/dashboards/project/shares/shares/forms.py +++ b/manila_ui/dashboards/project/shares/shares/forms.py @@ -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) diff --git a/manila_ui/dashboards/project/shares/shares/tables.py b/manila_ui/dashboards/project/shares/shares/tables.py index 7c71e369..59e18e0c 100644 --- a/manila_ui/dashboards/project/shares/shares/tables.py +++ b/manila_ui/dashboards/project/shares/shares/tables.py @@ -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, diff --git a/manila_ui/dashboards/project/shares/shares/views.py b/manila_ui/dashboards/project/shares/shares/views.py index 7fa96814..2ab28bfa 100644 --- a/manila_ui/dashboards/project/shares/shares/views.py +++ b/manila_ui/dashboards/project/shares/shares/views.py @@ -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, + } diff --git a/manila_ui/dashboards/project/shares/templates/shares/shares/_revert.html b/manila_ui/dashboards/project/shares/templates/shares/shares/_revert.html new file mode 100644 index 00000000..b5d9724d --- /dev/null +++ b/manila_ui/dashboards/project/shares/templates/shares/shares/_revert.html @@ -0,0 +1,6 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +

{% trans "Description" %}:

+

{% trans "From here you can revert the share to its latest snapshot." %}

+{% endblock %} diff --git a/manila_ui/dashboards/project/shares/templates/shares/shares/revert.html b/manila_ui/dashboards/project/shares/templates/shares/shares/revert.html new file mode 100644 index 00000000..d5973eb2 --- /dev/null +++ b/manila_ui/dashboards/project/shares/templates/shares/shares/revert.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Revert Share" %}{% endblock %} + +{% block main %} + {% include 'project/shares/shares/_revert.html' %} +{% endblock %} diff --git a/manila_ui/dashboards/project/shares/urls.py b/manila_ui/dashboards/project/shares/urls.py index ac625193..ed8ee9a1 100644 --- a/manila_ui/dashboards/project/shares/urls.py +++ b/manila_ui/dashboards/project/shares/urls.py @@ -79,6 +79,9 @@ urlpatterns = [ url(r'^(?P[^/]+)/extend/$', shares_views.ExtendView.as_view(), name='extend'), + url(r'^(?P[^/]+)/revert/$', + shares_views.RevertView.as_view(), + name='revert'), url(r'^(?P[^/]+)/snapshot_rules/$', snapshot_views.ManageRulesView.as_view(), name='snapshot_manage_rules'), diff --git a/manila_ui/tests/api/test_manila.py b/manila_ui/tests/api/test_manila.py index d218d38d..11ef0ee0 100644 --- a/manila_ui/tests/api/test_manila.py +++ b/manila_ui/tests/api/test_manila.py @@ -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' diff --git a/manila_ui/tests/dashboards/project/shares/shares/tests.py b/manila_ui/tests/dashboards/project/shares/shares/tests.py index 842f481e..d2706d15 100644 --- a/manila_ui/tests/dashboards/project/shares/shares/tests.py +++ b/manila_ui/tests/dashboards/project/shares/shares/tests.py @@ -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, diff --git a/releasenotes/notes/add-revert-share-to-snapshot-support-cba6227cef75c1a7.yaml b/releasenotes/notes/add-revert-share-to-snapshot-support-cba6227cef75c1a7.yaml new file mode 100644 index 00000000..d933d551 --- /dev/null +++ b/releasenotes/notes/add-revert-share-to-snapshot-support-cba6227cef75c1a7.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added possibility to revert share to its latest snapshot.