API 2.69,Manila client support recycle bin
The end user can soft delete or restore share by manila client. and can list shares in recycle bin. update micversion to 2.69 Depends-On: Ic838eec5fea890be6513514053329b1d2d86b3ba Partially-Implements: blueprint manila-share-support-recycle-bin Change-Id: Ic4748b59e2d99388c832969b7bef537b4115e4b1
This commit is contained in:
parent
21f2d69997
commit
ba45f40e12
@ -27,7 +27,7 @@ from manilaclient import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
MAX_VERSION = '2.67'
|
||||
MAX_VERSION = '2.69'
|
||||
MIN_VERSION = '2.0'
|
||||
DEPRECATED_VERSION = '1.0'
|
||||
_VERSIONED_METHOD_MAP = {}
|
||||
|
@ -24,6 +24,7 @@ import copy
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from manilaclient import api_versions
|
||||
from manilaclient.common import cliutils
|
||||
from manilaclient import exceptions
|
||||
from manilaclient import utils
|
||||
@ -237,7 +238,14 @@ class ManagerWithFind(Manager):
|
||||
searches = list(kwargs.items())
|
||||
|
||||
search_opts = {'all_tenants': 1}
|
||||
for obj in self.list(search_opts=search_opts):
|
||||
resources = self.list(search_opts=search_opts)
|
||||
if ('v2.shares.ShareManager' in str(self.__class__) and
|
||||
self.api_version >= api_versions.APIVersion("2.69")):
|
||||
search_opts_2 = {'all_tenants': 1,
|
||||
'is_soft_deleted': True}
|
||||
shares_soft_deleted = self.list(search_opts=search_opts_2)
|
||||
resources += shares_soft_deleted
|
||||
for obj in resources:
|
||||
try:
|
||||
if all(getattr(obj, attr) == value
|
||||
for (attr, value) in searches):
|
||||
|
@ -354,6 +354,20 @@ class BaseTestCase(base.ClientTestBase):
|
||||
client.delete_share(shares_to_delete, share_group_id=share_group_id,
|
||||
wait=wait, microversion=microversion)
|
||||
|
||||
@classmethod
|
||||
def soft_delete_share(cls, shares_to_soft_delete,
|
||||
client=None, microversion=None):
|
||||
client = client or cls.get_admin_client()
|
||||
client.soft_delete_share(shares_to_soft_delete,
|
||||
microversion=microversion)
|
||||
|
||||
@classmethod
|
||||
def restore_share(cls, shares_to_restore,
|
||||
client=None, microversion=None):
|
||||
client = client or cls.get_admin_client()
|
||||
client.restore_share(shares_to_restore,
|
||||
microversion=microversion)
|
||||
|
||||
@classmethod
|
||||
def _determine_share_network_to_use(cls, client, share_type,
|
||||
microversion=None):
|
||||
|
@ -869,12 +869,45 @@ class ManilaCLIClient(base.CLIClient):
|
||||
cmd += '--wait '
|
||||
return self.manila(cmd, microversion=microversion)
|
||||
|
||||
def list_shares(self, all_tenants=False, filters=None, columns=None,
|
||||
is_public=False, microversion=None):
|
||||
@not_found_wrapper
|
||||
@forbidden_wrapper
|
||||
def soft_delete_share(self, shares, microversion=None):
|
||||
"""Soft Delete share[s] by Names or IDs.
|
||||
|
||||
:param shares: either str or list of str that can be either Name
|
||||
or ID of a share(s) that should be soft deleted.
|
||||
"""
|
||||
if not isinstance(shares, list):
|
||||
shares = [shares]
|
||||
cmd = 'soft-delete '
|
||||
for share in shares:
|
||||
cmd += '%s ' % share
|
||||
return self.manila(cmd, microversion=microversion)
|
||||
|
||||
@not_found_wrapper
|
||||
@forbidden_wrapper
|
||||
def restore_share(self, shares, microversion=None):
|
||||
"""Restore share[s] by Names or IDs.
|
||||
|
||||
:param shares: either str or list of str that can be either Name
|
||||
or ID of a share(s) that should be soft deleted.
|
||||
"""
|
||||
if not isinstance(shares, list):
|
||||
shares = [shares]
|
||||
cmd = 'restore '
|
||||
for share in shares:
|
||||
cmd += '%s ' % share
|
||||
return self.manila(cmd, microversion=microversion)
|
||||
|
||||
def list_shares(self, all_tenants=False, is_soft_deleted=False,
|
||||
filters=None, columns=None, is_public=False,
|
||||
microversion=None):
|
||||
"""List shares.
|
||||
|
||||
:param all_tenants: bool -- whether to list shares that belong
|
||||
only to current project or for all tenants.
|
||||
:param is_soft_deleted: bool -- whether to list shares that has
|
||||
been soft deleted to recycle bin.
|
||||
:param filters: dict -- filters for listing of shares.
|
||||
Example, input:
|
||||
{'project_id': 'foo'}
|
||||
@ -886,12 +919,15 @@ class ManilaCLIClient(base.CLIClient):
|
||||
Example, "--columns Name,Size"
|
||||
:param is_public: bool -- should list public shares or not.
|
||||
Default is False.
|
||||
:param microversion: str -- the request api version.
|
||||
"""
|
||||
cmd = 'list '
|
||||
if all_tenants:
|
||||
cmd += '--all-tenants '
|
||||
if is_public:
|
||||
cmd += '--public '
|
||||
if is_soft_deleted:
|
||||
cmd += '--soft-deleted '
|
||||
if filters and isinstance(filters, dict):
|
||||
for k, v in filters.items():
|
||||
cmd += '%(k)s=%(v)s ' % {
|
||||
@ -947,6 +983,46 @@ class ManilaCLIClient(base.CLIClient):
|
||||
SHARE, res_id=share, interval=5, timeout=300,
|
||||
microversion=microversion)
|
||||
|
||||
def wait_for_share_soft_deletion(self, share_id, microversion=None):
|
||||
body = self.get_share(share_id, microversion=microversion)
|
||||
is_soft_deleted = body['is_soft_deleted']
|
||||
start = int(time.time())
|
||||
|
||||
while is_soft_deleted == "False":
|
||||
time.sleep(self.build_interval)
|
||||
body = self.get_share(share_id, microversion=microversion)
|
||||
is_soft_deleted = body['is_soft_deleted']
|
||||
|
||||
if is_soft_deleted == "True":
|
||||
return
|
||||
|
||||
if int(time.time()) - start >= self.build_timeout:
|
||||
message = ("Share %(share_id)s failed to be soft deleted "
|
||||
"within the required time %(build_timeout)s." %
|
||||
{"share_id": share_id,
|
||||
"build_timeout": self.build_timeout})
|
||||
raise tempest_lib_exc.TimeoutException(message)
|
||||
|
||||
def wait_for_share_restore(self, share_id, microversion=None):
|
||||
body = self.get_share(share_id, microversion=microversion)
|
||||
is_soft_deleted = body['is_soft_deleted']
|
||||
start = int(time.time())
|
||||
|
||||
while is_soft_deleted == "True":
|
||||
time.sleep(self.build_interval)
|
||||
body = self.get_share(share_id, microversion=microversion)
|
||||
is_soft_deleted = body['is_soft_deleted']
|
||||
|
||||
if is_soft_deleted == "False":
|
||||
return
|
||||
|
||||
if int(time.time()) - start >= self.build_timeout:
|
||||
message = ("Share %(share_id)s failed to be restored "
|
||||
"within the required time %(build_timeout)s." %
|
||||
{"share_id": share_id,
|
||||
"build_timeout": self.build_timeout})
|
||||
raise tempest_lib_exc.TimeoutException(message)
|
||||
|
||||
def wait_for_resource_status(self, resource_id, status, microversion=None,
|
||||
resource_type="share"):
|
||||
"""Waits for a share to reach a given status."""
|
||||
|
@ -125,6 +125,42 @@ class SharesReadWriteBase(base.BaseTestCase):
|
||||
self.user_client.get_share,
|
||||
share['id'])
|
||||
|
||||
def test_create_soft_delete_and_restore_share(self):
|
||||
self.skip_if_microversion_not_supported('2.69')
|
||||
microversion = '2.69'
|
||||
description = data_utils.rand_name('we-wait-until-share-is-ready')
|
||||
|
||||
share = self.create_share(self.protocol,
|
||||
name='share_name',
|
||||
description=description,
|
||||
use_wait_option=True,
|
||||
client=self.user_client)
|
||||
|
||||
self.assertEqual("available", share['status'])
|
||||
|
||||
# soft delete the share to recycle bin
|
||||
self.soft_delete_share([share['id']], client=self.user_client,
|
||||
microversion=microversion)
|
||||
self.user_client.wait_for_share_soft_deletion(share['id'])
|
||||
|
||||
# get shares list in recycle bin
|
||||
result = self.user_client.list_shares(is_soft_deleted=True,
|
||||
microversion=microversion)
|
||||
share_ids = [sh['ID'] for sh in result]
|
||||
# check share is in recycle bin
|
||||
self.assertIn(share['id'], share_ids)
|
||||
|
||||
# restore the share from recycle bin
|
||||
self.restore_share([share['id']], client=self.user_client,
|
||||
microversion=microversion)
|
||||
self.user_client.wait_for_share_restore(share['id'])
|
||||
|
||||
result1 = self.user_client.list_shares(is_soft_deleted=True,
|
||||
microversion=microversion)
|
||||
share_ids1 = [sh['ID'] for sh in result1]
|
||||
# check share not in recycle bin
|
||||
self.assertNotIn(share['id'], share_ids1)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class SharesTestMigration(base.BaseTestCase):
|
||||
|
@ -126,6 +126,8 @@ class SharesListReadWriteTest(base.BaseTestCase):
|
||||
self.admin_private_description = data_utils.rand_name(
|
||||
'autotest_admin_private_share_description')
|
||||
|
||||
self.soft_name = data_utils.rand_name('soft_delete_share_name')
|
||||
|
||||
self.admin_private_share = self.create_share(
|
||||
name=self.admin_private_name,
|
||||
description=self.admin_private_description,
|
||||
@ -146,14 +148,25 @@ class SharesListReadWriteTest(base.BaseTestCase):
|
||||
public=True,
|
||||
client=self.admin_client)
|
||||
|
||||
self.wait_soft_delete_share = self.create_share(
|
||||
name=self.soft_name,
|
||||
public=False,
|
||||
client=self.get_user_client(),
|
||||
wait_for_creation=False)
|
||||
|
||||
self.shares_created = (self.private_share['id'],
|
||||
self.public_share['id'],
|
||||
self.admin_private_share['id'])
|
||||
self.admin_private_share['id'],
|
||||
self.wait_soft_delete_share['id'])
|
||||
|
||||
for share_id in self.shares_created:
|
||||
self.admin_client.wait_for_resource_status(
|
||||
share_id, constants.STATUS_AVAILABLE)
|
||||
|
||||
self.soft_delete_share([self.wait_soft_delete_share['id']],
|
||||
client=self.get_user_client(),
|
||||
microversion='2.69')
|
||||
|
||||
def _list_shares(self, filters=None):
|
||||
filters = filters or dict()
|
||||
shares = self.user_client.list_shares(filters=filters)
|
||||
@ -349,3 +362,9 @@ class SharesListReadWriteTest(base.BaseTestCase):
|
||||
self.assertEqual(1, len(shares))
|
||||
self.assertTrue(
|
||||
any(self.private_share['id'] == s['ID'] for s in shares))
|
||||
|
||||
def test_list_shares_in_recycle_bin(self):
|
||||
shares = self.user_client.list_shares(is_soft_deleted=True)
|
||||
|
||||
self.assertTrue(
|
||||
any(self.wait_soft_delete_share['id'] == s['ID'] for s in shares))
|
||||
|
@ -66,4 +66,5 @@ class BaseTest(utils.TestCase):
|
||||
def test_findall_with_all_tenants(self):
|
||||
cs.shares.list = mock.Mock(return_value=[])
|
||||
cs.shares.findall()
|
||||
cs.shares.list.assert_called_once_with(search_opts={'all_tenants': 1})
|
||||
cs.shares.list.assert_called_with(
|
||||
search_opts={'all_tenants': 1, 'is_soft_deleted': True})
|
||||
|
@ -543,6 +543,8 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
|
||||
assert 'host' in body[action]
|
||||
elif action == 'reset_task_state':
|
||||
assert 'task_state' in body[action]
|
||||
elif action in ('soft_delete', 'restore'):
|
||||
assert body[action] is None
|
||||
else:
|
||||
raise AssertionError("Unexpected share action: %s" % action)
|
||||
return (resp, {}, _body)
|
||||
|
@ -162,6 +162,18 @@ class SharesTest(utils.TestCase):
|
||||
cs.shares.delete(share)
|
||||
cs.assert_called('DELETE', '/shares/1234')
|
||||
|
||||
def test_soft_delete_share(self):
|
||||
share = cs.shares.get('1234')
|
||||
cs.shares.soft_delete(share)
|
||||
body = {"soft_delete": None}
|
||||
cs.assert_called('POST', '/shares/1234/action', body=body)
|
||||
|
||||
def test_restore_share(self):
|
||||
share = cs.shares.get('1234')
|
||||
cs.shares.restore(share)
|
||||
body = {"restore": None}
|
||||
cs.assert_called('POST', '/shares/1234/action', body=body)
|
||||
|
||||
@ddt.data(
|
||||
("2.6", "/os-share-manage", None),
|
||||
("2.7", "/shares/manage", None),
|
||||
@ -299,6 +311,37 @@ class SharesTest(utils.TestCase):
|
||||
cs.shares.list(detailed=False)
|
||||
cs.assert_called('GET', '/shares?is_public=True')
|
||||
|
||||
@ddt.data("1.0", "2.35", "2.69")
|
||||
def test_list_shares_index_diff_api_version(self, microversion):
|
||||
version = api_versions.APIVersion(microversion)
|
||||
mock_microversion = mock.Mock(api_version=version)
|
||||
manager = shares.ShareManager(api=mock_microversion)
|
||||
search_opts1 = {}
|
||||
search_opts2 = {
|
||||
'export_location': 'fake_export_id',
|
||||
}
|
||||
search_opts3 = {
|
||||
'export_location': 'fake_export_id',
|
||||
'is_soft_deleted': 'True',
|
||||
}
|
||||
|
||||
with mock.patch.object(manager, "do_list",
|
||||
mock.Mock(return_value="fake")):
|
||||
manager.list(detailed=False, search_opts=search_opts3)
|
||||
|
||||
if version >= api_versions.APIVersion('2.69'):
|
||||
manager.do_list.assert_called_once_with(
|
||||
detailed=False, search_opts=search_opts3,
|
||||
sort_key=None, sort_dir=None)
|
||||
elif version >= api_versions.APIVersion('2.35'):
|
||||
manager.do_list.assert_called_once_with(
|
||||
detailed=False, search_opts=search_opts2,
|
||||
sort_key=None, sort_dir=None)
|
||||
else:
|
||||
manager.do_list.assert_called_once_with(
|
||||
detailed=False, search_opts=search_opts1,
|
||||
sort_key=None, sort_dir=None)
|
||||
|
||||
def test_list_shares_index_with_search_opts(self):
|
||||
search_opts = {
|
||||
'fake_str': 'fake_str_value',
|
||||
@ -332,6 +375,21 @@ class SharesTest(utils.TestCase):
|
||||
'GET', ('/shares?export_location_' + filter_type + '='
|
||||
+ value + '&is_public=True'))
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_list_shares_index_with_is_soft_deleted(self, detailed):
|
||||
search_opts = {
|
||||
'is_soft_deleted': 'True',
|
||||
}
|
||||
cs.shares.list(detailed=detailed, search_opts=search_opts)
|
||||
if detailed:
|
||||
cs.assert_called(
|
||||
'GET', ('/shares/detail?is_public=True'
|
||||
+ '&is_soft_deleted=True'))
|
||||
else:
|
||||
cs.assert_called(
|
||||
'GET', ('/shares?is_public=True'
|
||||
+ '&is_soft_deleted=True'))
|
||||
|
||||
def test_list_shares_detailed(self):
|
||||
search_opts = {
|
||||
'with_count': 'True',
|
||||
|
@ -1034,6 +1034,16 @@ class ShellTest(test_utils.TestCase):
|
||||
self.assertEqual(len(shares_to_delete),
|
||||
fake_manager.force_delete.call_count)
|
||||
|
||||
def test_soft_delete(self):
|
||||
self.run_command('soft-delete 1234')
|
||||
expected = {'soft_delete': None}
|
||||
self.assert_called('POST', '/shares/1234/action', body=expected)
|
||||
|
||||
def test_restore(self):
|
||||
self.run_command('restore 1234')
|
||||
expected = {'restore': None}
|
||||
self.assert_called('POST', '/shares/1234/action', body=expected)
|
||||
|
||||
def test_list_snapshots(self):
|
||||
self.run_command('snapshot-list')
|
||||
self.assert_called('GET', '/snapshots/detail')
|
||||
|
@ -111,6 +111,14 @@ class Share(base.Resource):
|
||||
"""Reverts a share (in place) to a snapshot."""
|
||||
self.manager.revert_to_snapshot(self, snapshot)
|
||||
|
||||
def soft_delete(self):
|
||||
"""Soft delete a share to recycle bin"""
|
||||
self.manager.soft_delete(self)
|
||||
|
||||
def restore(self):
|
||||
"""Restore a share from recycle bin"""
|
||||
self.manager.restore(self)
|
||||
|
||||
|
||||
class ShareManager(base.ManagerWithFind):
|
||||
"""Manage :class:`Share` resources."""
|
||||
@ -341,13 +349,22 @@ class ShareManager(base.ManagerWithFind):
|
||||
"""Get a list of all shares."""
|
||||
search_opts = search_opts or {}
|
||||
search_opts.pop("export_location", None)
|
||||
search_opts.pop("is_soft_deleted", None)
|
||||
return self.do_list(detailed=detailed, search_opts=search_opts,
|
||||
sort_key=sort_key, sort_dir=sort_dir)
|
||||
|
||||
@api_versions.wraps("2.35") # noqa
|
||||
@api_versions.wraps("2.35", "2.68") # noqa
|
||||
def list(self, detailed=True, search_opts=None, # noqa
|
||||
sort_key=None, sort_dir=None):
|
||||
"""Get a list of all shares."""
|
||||
search_opts.pop("is_soft_deleted", None)
|
||||
return self.do_list(detailed=detailed, search_opts=search_opts,
|
||||
sort_key=sort_key, sort_dir=sort_dir)
|
||||
|
||||
@api_versions.wraps("2.69") # noqa
|
||||
def list(self, detailed=True, search_opts=None, # noqa
|
||||
sort_key=None, sort_dir=None):
|
||||
"""Get a list of all shares."""
|
||||
return self.do_list(detailed=detailed, search_opts=search_opts,
|
||||
sort_key=sort_key, sort_dir=sort_dir)
|
||||
|
||||
@ -371,6 +388,7 @@ class ShareManager(base.ManagerWithFind):
|
||||
- (('share_network_id', 'share_network'), text)
|
||||
- (('share_type_id', 'share_type'), text)
|
||||
- (('snapshot_id', 'snapshot'), text)
|
||||
- ('is_soft_deleted', bool)
|
||||
Note, that member context will have restricted set of
|
||||
available search opts. For admin context filtering also available
|
||||
by each share attr from its Model. So, this list is not full for
|
||||
@ -451,6 +469,22 @@ class ShareManager(base.ManagerWithFind):
|
||||
def force_delete(self, share): # noqa
|
||||
return self._do_force_delete(share, "force_delete")
|
||||
|
||||
@api_versions.wraps("2.69")
|
||||
def soft_delete(self, share):
|
||||
"""Soft delete a share - share will go to recycle bin.
|
||||
|
||||
:param share: either share object or text with its ID.
|
||||
"""
|
||||
return self._action("soft_delete", share)
|
||||
|
||||
@api_versions.wraps("2.69")
|
||||
def restore(self, share):
|
||||
"""Restore a share - share will restore from recycle bin.
|
||||
|
||||
:param share: either share object or text with its ID.
|
||||
"""
|
||||
return self._action("restore", share)
|
||||
|
||||
@staticmethod
|
||||
def _validate_common_name(access):
|
||||
if len(access) == 0 or len(access) > 64:
|
||||
|
@ -1844,6 +1844,56 @@ def do_force_delete(cs, args):
|
||||
print(e, file=sys.stderr)
|
||||
|
||||
|
||||
@cliutils.arg(
|
||||
'share',
|
||||
metavar='<share>',
|
||||
nargs='+',
|
||||
help='Name or ID of the share(s).')
|
||||
@cliutils.service_type('sharev2')
|
||||
@api_versions.wraps("2.69")
|
||||
def do_soft_delete(cs, args):
|
||||
"""Soft delete one or more shares."""
|
||||
failure_count = 0
|
||||
|
||||
for share in args.share:
|
||||
try:
|
||||
share_ref = _find_share(cs, share)
|
||||
cs.shares.soft_delete(share_ref)
|
||||
except Exception as e:
|
||||
failure_count += 1
|
||||
print("Soft deletion of share %s failed: %s" % (share, e),
|
||||
file=sys.stderr)
|
||||
|
||||
if failure_count == len(args.share):
|
||||
raise exceptions.CommandError("Unable to soft delete any of the "
|
||||
"specified shares.")
|
||||
|
||||
|
||||
@cliutils.arg(
|
||||
'share',
|
||||
metavar='<share>',
|
||||
nargs='+',
|
||||
help='Name or ID of the share(s).')
|
||||
@cliutils.service_type('sharev2')
|
||||
@api_versions.wraps("2.69")
|
||||
def do_restore(cs, args):
|
||||
"""Restore one or more shares from recycle bin."""
|
||||
failure_count = 0
|
||||
|
||||
for share in args.share:
|
||||
try:
|
||||
share_ref = _find_share(cs, share)
|
||||
cs.shares.restore(share_ref)
|
||||
except Exception as e:
|
||||
failure_count += 1
|
||||
print("Restoration of share %s failed: %s" % (share, e),
|
||||
file=sys.stderr)
|
||||
|
||||
if failure_count == len(args.share):
|
||||
raise exceptions.CommandError("Unable to restore any of the "
|
||||
"specified shares.")
|
||||
|
||||
|
||||
@api_versions.wraps("1.0", "2.8")
|
||||
@cliutils.arg(
|
||||
'share',
|
||||
@ -2313,6 +2363,12 @@ def do_snapshot_access_list(cs, args):
|
||||
default=False,
|
||||
help='Display total number of shares to return. '
|
||||
'Available only for microversion >= 2.42.')
|
||||
@cliutils.arg(
|
||||
'--soft-deleted', '--soft_deleted',
|
||||
action='store_true',
|
||||
help='Get shares in recycle bin. If this parameter is set to '
|
||||
'True(Default=False), will only show shares in recycle bin. '
|
||||
'Available only for microversion >= 2.69.')
|
||||
@cliutils.service_type('sharev2')
|
||||
def do_list(cs, args):
|
||||
"""List NAS shares with filters."""
|
||||
@ -2389,6 +2445,15 @@ def do_list(cs, args):
|
||||
"Display total number of shares is only "
|
||||
"available with manila API version >= 2.42")
|
||||
|
||||
if cs.api_version.matches(api_versions.APIVersion("2.69"),
|
||||
api_versions.APIVersion()):
|
||||
if args.soft_deleted:
|
||||
search_opts['is_soft_deleted'] = args.soft_deleted
|
||||
elif args.soft_deleted:
|
||||
raise exceptions.CommandError(
|
||||
"Filtering by is_soft_deleted is only "
|
||||
"available with manila API version >= 2.69")
|
||||
|
||||
if share_group:
|
||||
search_opts['share_group_id'] = share_group.id
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- Added CLI commands to soft delete share.
|
||||
- Added CLI commands to restore share.
|
||||
- Added CLI commands to query shares in recycle bin.
|
Loading…
Reference in New Issue
Block a user