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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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/$',
|
||||
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'),
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added possibility to revert share to its latest snapshot.
|
Loading…
Reference in New Issue
Block a user