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:
haixin 2021-07-20 15:38:52 +08:00 committed by haixin
parent 21f2d69997
commit ba45f40e12
13 changed files with 335 additions and 7 deletions

View File

@ -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 = {}

View File

@ -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):

View File

@ -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):

View File

@ -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."""

View File

@ -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):

View File

@ -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))

View File

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

View File

@ -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)

View File

@ -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',

View File

@ -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')

View File

@ -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:

View File

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

View File

@ -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.