Update micversion to 2.77,support share transfer between project.

Depends-On: I8facf9112a6b09e6b7aed9956c0a87fb5f1fc31f
Partially-Implements: blueprint transfer-share-between-project

Change-Id: I3d5d0a57574edd350dc79a52879aa67016122f56
This commit is contained in:
haixin 2022-08-24 14:28:23 +08:00
parent 07486204b6
commit d54326ff7c
18 changed files with 1350 additions and 1 deletions

View File

@ -27,7 +27,7 @@ from manilaclient import utils
LOG = logging.getLogger(__name__)
MAX_VERSION = '2.75'
MAX_VERSION = '2.77'
MIN_VERSION = '2.0'
DEPRECATED_VERSION = '1.0'
_VERSIONED_METHOD_MAP = {}

View File

@ -193,6 +193,9 @@ class Manager(utils.HookableMixin):
with self.completion_cache('uuid', self.resource_class, mode="a"):
return self.resource_class(self, body[response_key])
def _accept(self, url, body):
resp, body = self.api.client.post(url, body=body)
def _delete(self, url):
resp, body = self.api.client.delete(url)

View File

@ -68,6 +68,17 @@ SHARE_GROUP_SNAPSHOT_SORT_KEY_VALUES = (
'share_group_id',
)
SHARE_TRANSFER_SORT_KEY_VALUES = (
'id',
'resource_type',
'resource_id',
'name',
'source_project_id',
'destination_project_id',
'created_at',
'expires_at',
)
TASK_STATE_MIGRATION_SUCCESS = 'migration_success'
TASK_STATE_MIGRATION_ERROR = 'migration_error'
TASK_STATE_MIGRATION_CANCELLED = 'migration_cancelled'
@ -119,3 +130,4 @@ GROUP_BOOL_SPECS = (
REPLICA_GRADUATION_VERSION = '2.56'
REPLICA_PRE_GRADUATION_VERSION = '2.55'
SHARE_TRANSFER_VERSION = '2.77'

View File

@ -0,0 +1,272 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils as osc_utils
from manilaclient.common._i18n import _
from manilaclient.common.apiclient import utils as apiutils
from manilaclient.common import constants
LOG = logging.getLogger(__name__)
TRANSFER_DETAIL_ATTRIBUTES = [
'id',
'created_at',
'name',
'resource_type',
'resource_id',
'source_project_id',
'destination_project_id',
'accepted',
'expires_at'
]
TRANSFER_SUMMARY_ATTRIBUTES = [
'id',
'name',
'resource_type',
'resource_id',
]
class CreateShareTransfer(command.ShowOne):
"""Create a new share transfer."""
_description = _("Create a new share transfer")
def get_parser(self, prog_name):
parser = super(CreateShareTransfer, self).get_parser(prog_name)
parser.add_argument(
'share',
metavar='<share>',
help='Name or ID of share to transfer.')
parser.add_argument(
'--name',
metavar='<name>',
default=None,
help='Transfer name. Default=None.')
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
share = osc_utils.find_resource(share_client.shares,
parsed_args.share)
transfer = share_client.transfers.create(
share.id, name=parsed_args.name)
transfer._info.pop('links', None)
return self.dict2columns(transfer._info)
class DeleteShareTransfer(command.Command):
"""Remove one or more transfers."""
_description = _("Remove one or more transfers")
def get_parser(self, prog_name):
parser = super(DeleteShareTransfer, self).get_parser(prog_name)
parser.add_argument(
'transfer',
metavar='<transfer>',
nargs='+',
help='Name(s) or ID(s) of the transfer(s).')
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
failure_count = 0
for transfer in parsed_args.transfer:
try:
transfer_obj = apiutils.find_resource(
share_client.transfers,
transfer)
share_client.transfers.delete(transfer_obj.id)
except Exception as e:
failure_count += 1
LOG.error(_(
"Failed to delete %(transfer)s: %(e)s"),
{'transfer': transfer, 'e': e})
if failure_count > 0:
raise exceptions.CommandError(_(
"Unable to delete some or all of the specified transfers."))
class ListShareTransfer(command.Lister):
"""Lists all transfers."""
_description = _("Lists all transfers")
def get_parser(self, prog_name):
parser = super(ListShareTransfer, self).get_parser(prog_name)
parser.add_argument(
'--all-projects',
action='store_true',
help=_("Shows details for all tenants. (Admin only).")
)
parser.add_argument(
'--name',
metavar='<name>',
default=None,
help='Filter share transfers by name. Default=None.')
parser.add_argument(
'--id',
metavar='<id>',
default=None,
help='Filter share transfers by ID. Default=None.')
parser.add_argument(
'--resource-type', '--resource_type',
metavar='<resource_type>',
default=None,
help='Filter share transfers by resource type, '
'which can be share. Default=None.')
parser.add_argument(
'--resource-id', '--resource_id',
metavar='<resource_id>',
default=None,
help='Filter share transfers by resource ID. Default=None.')
parser.add_argument(
'--source-project-id', '--source_project_id',
metavar='<source_project_id>',
default=None,
help='Filter share transfers by ID of the Project that '
'initiated the transfer. Default=None.')
parser.add_argument(
'--limit',
metavar='<limit>',
type=int,
default=None,
help='Maximum number of transfer records to '
'return. (Default=None)')
parser.add_argument(
'--offset',
metavar="<offset>",
default=None,
help='Start position of transfer records listing.')
parser.add_argument(
'--sort-key', '--sort_key',
metavar='<sort_key>',
type=str,
default=None,
help='Key to be sorted, available keys are %(keys)s. '
'Default=None.'
% {'keys': constants.SHARE_TRANSFER_SORT_KEY_VALUES})
parser.add_argument(
'--sort-dir', '--sort_dir',
metavar='<sort_dir>',
type=str,
default=None,
help='Sort direction, available values are %(values)s. '
'OPTIONAL: Default=None.' % {
'values': constants.SORT_DIR_VALUES})
parser.add_argument(
'--detailed',
dest='detailed',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help="Show detailed information about filtered share transfers.")
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
columns = [
'ID',
'Name',
'Resource Type',
'Resource Id'
]
if parsed_args.detailed:
columns.extend(['Created At', 'Source Project Id',
'Destination Project Id', 'Accepted',
'Expires At'])
search_opts = {
'all_tenants': parsed_args.all_projects,
'id': parsed_args.id,
'name': parsed_args.name,
'limit': parsed_args.limit,
'offset': parsed_args.offset,
'resource_type': parsed_args.resource_type,
'resource_id': parsed_args.resource_id,
'source_project_id': parsed_args.source_project_id,
}
transfers = share_client.transfers.list(
detailed=parsed_args.detailed, search_opts=search_opts,
sort_key=parsed_args.sort_key, sort_dir=parsed_args.sort_dir)
return (columns, (osc_utils.get_item_properties
(m, columns) for m in transfers))
class ShowShareTransfer(command.ShowOne):
"""Show details about a share transfer."""
_description = _("Show details about a share transfer")
def get_parser(self, prog_name):
parser = super(ShowShareTransfer, self).get_parser(prog_name)
parser.add_argument(
'transfer',
metavar='<transfer>',
help=_('Name or ID of transfer to show.'))
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
transfer = apiutils.find_resource(
share_client.transfers,
parsed_args.transfer)
return (TRANSFER_DETAIL_ATTRIBUTES, osc_utils.get_dict_properties(
transfer._info, TRANSFER_DETAIL_ATTRIBUTES))
class AcceptShareTransfer(command.Command):
"""Accepts a share transfer."""
_description = _("Accepts a share transfer")
def get_parser(self, prog_name):
parser = super(AcceptShareTransfer, self).get_parser(prog_name)
parser.add_argument(
'transfer',
metavar='<transfer>',
help='ID of transfer to accept.')
parser.add_argument(
'auth_key',
metavar='<auth_key>',
help='Authentication key of transfer to accept.')
parser.add_argument(
'--clear-rules',
'--clear_rules',
dest='clear_rules',
action='store_true',
default=False,
help="Whether manila should clean up the access rules after the "
"transfer is complete. (Default=False)")
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
share_client.transfers.accept(
parsed_args.transfer, parsed_args.auth_key,
clear_access_rules=parsed_args.clear_rules)

View File

@ -368,6 +368,36 @@ class BaseTestCase(base.ClientTestBase):
client.restore_share(shares_to_restore,
microversion=microversion)
@classmethod
def create_share_transfer(cls, share_id, name=None,
client=None, microversion=None):
client = client or cls.get_admin_client()
return client.create_share_transfer(share_id, name=name,
microversion=microversion)
@classmethod
def delete_share_transfer(cls, transfer, client=None,
microversion=None):
client = client or cls.get_admin_client()
client.delete_share_transfer(transfer, microversion=microversion)
@classmethod
def get_share_transfer(cls, transfer, client=None, microversion=None):
client = client or cls.get_admin_client()
return client.get_share_transfer(transfer, microversion=microversion)
@classmethod
def list_share_transfer(cls, client=None, microversion=None):
client = client or cls.get_admin_client()
return client.list_share_transfer(microversion=microversion)
@classmethod
def accept_share_transfer(cls, transfer, auth_key,
client=None, microversion=None):
client = client or cls.get_admin_client()
client.accept_share_transfer(transfer, auth_key,
microversion=microversion)
@classmethod
def _determine_share_network_to_use(cls, client, share_type,
microversion=None):

View File

@ -37,6 +37,7 @@ SHARE_NETWORK_SUBNET = 'share_network_subnet'
SHARE_SERVER = 'share_server'
SNAPSHOT = 'snapshot'
SHARE_REPLICA = 'share_replica'
TRANSFER = 'transfer'
def not_found_wrapper(f):
@ -142,6 +143,8 @@ class ManilaCLIClient(base.CLIClient):
func = self.is_message_deleted
elif res_type == SHARE_REPLICA:
func = self.is_share_replica_deleted
elif res_type == TRANSFER:
func = self.is_share_transfer_deleted
else:
raise exceptions.InvalidResource(message=res_type)
@ -919,6 +922,55 @@ class ManilaCLIClient(base.CLIClient):
cmd += '%s ' % share
return self.manila(cmd, microversion=microversion)
def create_share_transfer(self, share_id, name=None,
microversion=None):
"""Create a share transfer.
:param share_id: ID of share.
":param name: name of transfer.
"""
cmd = 'share-transfer-create %s ' % share_id
if name:
cmd += '--name %s' % name
transfer_raw = self.manila(cmd, microversion=microversion)
transfer = output_parser.details(transfer_raw)
return transfer
def delete_share_transfer(self, transfer, microversion=None):
"""Delete a share transfer.
:param transfer: ID or name of share transfer.
"""
cmd = 'share-transfer-delete %s ' % transfer
self.manila(cmd, microversion=microversion)
def get_share_transfer(self, transfer, microversion=None):
"""Get a share transfer.
:param transfer: ID or name of share transfer.
"""
cmd = 'share-transfer-show %s ' % transfer
transfer_raw = self.manila(cmd, microversion=microversion)
transfer = output_parser.details(transfer_raw)
return transfer
def list_share_transfer(self, microversion=None):
"""Get a share transfer."""
cmd = 'share-transfer-list '
transfer_raw = self.manila(cmd, microversion=microversion)
transfers = utils.listing(transfer_raw)
return transfers
def accept_share_transfer(self, transfer, auth_key,
microversion=None):
"""Accept a share transfer.
:param transfer: ID or name of share transfer.
"""
cmd = 'share-transfer-accept %s %s' % (transfer, auth_key)
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):
@ -1003,6 +1055,26 @@ class ManilaCLIClient(base.CLIClient):
SHARE, res_id=share, interval=5, timeout=300,
microversion=microversion)
def is_share_transfer_deleted(self, transfer, microversion=None):
"""Says whether transfer is deleted or not.
:param transfer: str -- Name or ID of transfer
"""
try:
self.get_transfer(transfer, microversion=microversion)
return False
except tempest_lib_exc.NotFound:
return True
def wait_for_transfer_deletion(self, transfer, microversion=None):
"""Wait for transfer deletion by its Name or ID.
:param transfer: str -- Name or ID of transfer.
"""
self.wait_for_resource_deletion(
SHARE, res_id=transfer, 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']

View File

@ -302,6 +302,14 @@ class OSCClientTestBase(base.ClientTestBase):
return snapshot_object
def create_share_transfer(self, share, name=None, client=None):
name = name or data_utils.rand_name('autotest_share_transfer_name')
cmd = (f'transfer create {share} --name {name} ')
transfer_object = self.dict_result('share', cmd, client=client)
return transfer_object
def create_share_network(self, neutron_net_id=None,
neutron_subnet_id=None, name=None,
description=None, availability_zone=None,

View File

@ -0,0 +1,85 @@
# Copyright (c) 2022 China Telecom Digital Intelligence.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from manilaclient.tests.functional.osc import base
class TransfersCLITest(base.OSCClientTestBase):
def setUp(self):
super(TransfersCLITest, self).setUp()
self.share_type = self.create_share_type()
def test_transfer_create_list_show_delete(self):
share = self.create_share(share_type=self.share_type['name'],
wait_for_status='available',
client=self.user_client)
# create share transfer
self.create_share_transfer(share['id'], name='transfer_test')
self._wait_for_object_status('share', share['id'], 'awaiting_transfer')
# Get all transfers
transfers = self.listing_result(
'share', 'transfer list', client=self.user_client)
# We must have at least one transfer
self.assertTrue(len(transfers) > 0)
self.assertTableStruct(transfers, [
'ID',
'Name',
'Resource Type',
'Resource Id',
])
# grab the transfer we created
transfer = [transfer for transfer in transfers
if transfer['Resource Id'] == share['id']]
self.assertEqual(1, len(transfer))
show_transfer = self.dict_result('share',
f'transfer show {transfer[0]["ID"]}')
self.assertEqual(transfer[0]['ID'], show_transfer['id'])
expected_keys = (
'id', 'created_at', 'name', 'resource_type', 'resource_id',
'source_project_id', 'destination_project_id', 'accepted',
'expires_at',
)
for key in expected_keys:
self.assertIn(key, show_transfer)
# filtering by Resource ID
filtered_transfers = self.listing_result(
'share',
f'transfer list --resource-id {share["id"]}',
client=self.user_client)
self.assertEqual(1, len(filtered_transfers))
self.assertEqual(show_transfer['resource_id'], share["id"])
# finally delete transfer and share
self.openstack(f'share transfer delete {show_transfer["id"]}')
self._wait_for_object_status('share', share['id'], 'available')
def test_transfer_accept(self):
share = self.create_share(share_type=self.share_type['name'],
wait_for_status='available',
client=self.user_client)
# create share transfer
transfer = self.create_share_transfer(share['id'],
name='transfer_test')
self._wait_for_object_status('share', share['id'], 'awaiting_transfer')
# accept share transfer
self.openstack(
f'share transfer accept {transfer["id"]} {transfer["auth_key"]}')
self._wait_for_object_status('share', share['id'], 'available')

View File

@ -0,0 +1,95 @@
# Copyright (c) 2022 China Telecom Digital Intelligence.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from tempest.lib.common.utils import data_utils
from manilaclient.tests.functional import base
class ShareTransferTests(base.BaseTestCase):
"""Check of base share transfers command"""
def setUp(self):
super(ShareTransferTests, self).setUp()
self.share_type = self.create_share_type(
name=data_utils.rand_name('test_share_type'),
driver_handles_share_servers=False)
def test_transfer_create_list_show_delete(self):
"""Create, list, show and delete a share transfer"""
self.skip_if_microversion_not_supported('2.77')
share = self.create_share(
share_protocol='nfs',
size=1,
name=data_utils.rand_name('autotest_share_name'),
client=self.user_client,
share_type=self.share_type['ID'],
use_wait_option=True)
self.assertEqual("available", share['status'])
# create share transfer
transfer = self.create_share_transfer(share['id'],
name='test_share_transfer')
self.assertIn('auth_key', transfer)
# list share transfers
transfers = self.list_share_transfer()
# We must have at least one transfer
self.assertTrue(len(transfers) > 0)
# show the share transfer
transfer_show = self.get_share_transfer(transfer['id'])
self.assertEqual(transfer_show['name'], transfer['name'])
self.assertNotIn('auth_key', transfer_show)
# delete share transfer
self.delete_share_transfer(transfer['id'])
self.user_client.wait_for_transfer_deletion(transfer['id'])
share = self.user_client.get_share(share['id'])
self.assertEqual("available", share['status'])
# finally delete the share
self.user_client.delete_share(share['id'])
self.user_client.wait_for_share_deletion(share['id'])
def test_transfer_accept(self):
"""Show share transfer accept"""
self.skip_if_microversion_not_supported('2.77')
share = self.create_share(
share_protocol='nfs',
size=1,
name=data_utils.rand_name('autotest_share_name'),
client=self.user_client,
share_type=self.share_type['ID'],
use_wait_option=True)
self.assertEqual("available", share['status'])
# create share transfer
transfer = self.create_share_transfer(share['id'],
name='test_share_transfer')
share = self.user_client.get_share(share['id'])
transfer_id = transfer['id']
auth_key = transfer['auth_key']
self.assertEqual("share", transfer['resource_type'])
self.assertEqual('test_share_transfer', transfer['name'])
self.assertEqual("awaiting_transfer", share['status'])
# accept the share transfer
self.accept_share_transfer(transfer_id, auth_key)
# after accept complete, the transfer will be deleted.
self.user_client.wait_for_transfer_deletion(transfer_id)
share = self.user_client.get_share(share['id'])
# check share status roll back to available
self.assertEqual("available", share['status'])
# finally delete the share
self.user_client.delete_share(share['id'])
self.user_client.wait_for_share_deletion(share['id'])

View File

@ -30,6 +30,7 @@ class FakeShareClient(object):
self.auth_token = kwargs['token']
self.management_url = kwargs['endpoint']
self.shares = mock.Mock()
self.transfers = mock.Mock()
self.share_access_rules = mock.Mock()
self.share_groups = mock.Mock()
self.share_types = mock.Mock()
@ -585,6 +586,64 @@ class FakeShareSnapshot(object):
return share_snapshots
class FakeShareTransfer(object):
"""Fake a share transfer"""
@staticmethod
def create_one_transfer(attrs=None, methods=None):
"""Create a fake share transfer
:param Dictionary attrs:
A dictionary with all attributes
:return:
A FakeResource object, with id, resource and so on
"""
attrs = attrs or {}
methods = methods or {}
now_time = datetime.datetime.now()
delta_time = now_time + datetime.timedelta(minutes=5)
share_transfer = {
'accepted': 'False',
'auth_key': 'auth-key-' + uuid.uuid4().hex,
'created_at': now_time.isoformat(),
'destination_project_id': None,
'expires_at': delta_time.isoformat(),
'id': 'transfer-id-' + uuid.uuid4().hex,
'name': 'name-' + uuid.uuid4().hex,
'resource_id': 'resource-id-' + uuid.uuid4().hex,
'resource_type': 'share',
'source_project_id': 'source-project-id-' + uuid.uuid4().hex
}
share_transfer.update(attrs)
share_transfer = osc_fakes.FakeResource(info=copy.deepcopy(
share_transfer),
methods=methods,
loaded=True)
return share_transfer
@staticmethod
def create_share_transfers(attrs=None, count=2):
"""Create multiple fake transfers.
:param Dictionary attrs:
A dictionary with all attributes
:param Integer count:
The number of share transfers to be faked
:return:
A list of FakeResource objects
"""
share_transfers = []
for n in range(0, count):
share_transfers.append(
FakeShareSnapshot.create_one_snapshot(attrs))
return share_transfers
class FakeSnapshotAccessRule(object):
"""Fake one or more snapshot access rules"""

View File

@ -0,0 +1,289 @@
# Copyright (c) 2022 China Telecom Digital Intelligence.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from osc_lib import exceptions
from osc_lib import utils as oscutils
from manilaclient import api_versions
from manilaclient.osc.v2 import share_transfers as osc_share_transfers
from manilaclient.tests.unit.osc import osc_utils
from manilaclient.tests.unit.osc.v2 import fakes as manila_fakes
COLUMNS = [
'ID',
'Name',
'Resource Type',
'Resource Id',
'Created At',
'Source Project Id',
'Destination Project Id',
'Accepted',
'Expires At'
]
class TestShareTransfer(manila_fakes.TestShare):
def setUp(self):
super(TestShareTransfer, self).setUp()
self.shares_mock = self.app.client_manager.share.shares
self.shares_mock.reset_mock()
self.transfers_mock = self.app.client_manager.share.transfers
self.transfers_mock.reset_mock()
self.app.client_manager.share.api_version = api_versions.APIVersion(
api_versions.MAX_VERSION)
class TestShareTransferCreate(TestShareTransfer):
def setUp(self):
super(TestShareTransferCreate, self).setUp()
self.share = manila_fakes.FakeShare.create_one_share()
self.shares_mock.create.return_value = self.share
self.shares_mock.get.return_value = self.share
self.share_transfer = (
manila_fakes.FakeShareTransfer.create_one_transfer())
self.transfers_mock.get.return_value = self.share_transfer
self.transfers_mock.create.return_value = self.share_transfer
self.cmd = osc_share_transfers.CreateShareTransfer(self.app, None)
self.data = tuple(self.share_transfer._info.values())
self.columns = tuple(self.share_transfer._info.keys())
def test_share_transfer_create_missing_args(self):
arglist = []
verifylist = []
self.assertRaises(osc_utils.ParserException,
self.check_parser, self.cmd, arglist, verifylist)
def test_share_transfer_create_required_args(self):
arglist = [
self.share.id
]
verifylist = [
('share', self.share.id)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.transfers_mock.create.assert_called_with(
self.share.id,
name=None
)
self.assertCountEqual(self.columns, columns)
self.assertCountEqual(self.data, data)
class TestShareTransferDelete(TestShareTransfer):
def setUp(self):
super(TestShareTransferDelete, self).setUp()
self.transfer = (
manila_fakes.FakeShareTransfer.create_one_transfer())
self.transfers_mock.get.return_value = self.transfer
self.cmd = osc_share_transfers.DeleteShareTransfer(self.app, None)
def test_share_transfer_delete_missing_args(self):
arglist = []
verifylist = []
self.assertRaises(osc_utils.ParserException,
self.check_parser, self.cmd, arglist, verifylist)
def test_share_transfer_delete(self):
arglist = [
self.transfer.id
]
verifylist = [
('transfer', [self.transfer.id])
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.transfers_mock.delete.assert_called_with(self.transfer.id)
self.assertIsNone(result)
def test_share_transfer_delete_multiple(self):
transfers = (
manila_fakes.FakeShareTransfer.create_share_transfers(
count=2))
arglist = [
transfers[0].id,
transfers[1].id
]
verifylist = [
('transfer', [transfers[0].id, transfers[1].id])
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.assertEqual(self.transfers_mock.delete.call_count,
len(transfers))
self.assertIsNone(result)
def test_share_transfer_delete_exception(self):
arglist = [
self.transfer.id
]
verifylist = [
('transfer', [self.transfer.id])
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.transfers_mock.delete.side_effect = exceptions.CommandError()
self.assertRaises(exceptions.CommandError,
self.cmd.take_action,
parsed_args)
class TestShareTransferShow(TestShareTransfer):
def setUp(self):
super(TestShareTransferShow, self).setUp()
self.transfer = (
manila_fakes.FakeShareTransfer.create_one_transfer())
self.transfers_mock.get.return_value = self.transfer
self.cmd = osc_share_transfers.ShowShareTransfer(self.app, None)
self.data = self.transfer._info.values()
self.transfer._info.pop('auth_key')
self.columns = self.transfer._info.keys()
def test_share_transfer_show_missing_args(self):
arglist = []
verifylist = []
self.assertRaises(osc_utils.ParserException,
self.check_parser, self.cmd, arglist, verifylist)
def test_share_transfer_show(self):
arglist = [
self.transfer.id
]
verifylist = [
('transfer', self.transfer.id)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.transfers_mock.get.assert_called_with(self.transfer.id)
self.assertCountEqual(self.columns, columns)
self.assertCountEqual(self.data, data)
class TestShareTransferList(TestShareTransfer):
def setUp(self):
super(TestShareTransferList, self).setUp()
self.transfers = (
manila_fakes.FakeShareTransfer.create_share_transfers(
count=2))
self.transfers_mock.list.return_value = self.transfers
self.values = (oscutils.get_dict_properties(
m._info, COLUMNS) for m in self.transfers)
self.cmd = osc_share_transfers.ListShareTransfer(self.app, None)
def test_list_transfers(self):
arglist = [
'--detailed'
]
verifylist = [
('detailed', True)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.transfers_mock.list.assert_called_with(
detailed=1,
search_opts={
'all_tenants': False,
'id': None,
'name': None,
'limit': None,
'offset': None,
'resource_type': None,
'resource_id': None,
'source_project_id': None},
sort_key=None,
sort_dir=None
)
self.assertEqual(COLUMNS, columns)
self.assertEqual(list(self.values), list(data))
class TestShareTransferAccept(TestShareTransfer):
def setUp(self):
super(TestShareTransferAccept, self).setUp()
self.transfer = (
manila_fakes.FakeShareTransfer.create_one_transfer())
self.transfers_mock.get.return_value = self.transfer
self.cmd = osc_share_transfers.AcceptShareTransfer(self.app, None)
def test_share_transfer_accept_missing_args(self):
arglist = []
verifylist = []
self.assertRaises(osc_utils.ParserException,
self.check_parser, self.cmd, arglist, verifylist)
def test_share_transfer_accept(self):
arglist = [
self.transfer.id,
self.transfer.auth_key
]
verifylist = [
('transfer', self.transfer.id),
('auth_key', self.transfer.auth_key)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.transfers_mock.accept.assert_called_with(self.transfer.id,
self.transfer.auth_key,
clear_access_rules=False)
self.assertIsNone(result)

View File

@ -1317,6 +1317,19 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
'request_id': 'req-936666d2-4c8f-4e41-9ac9-237b43f8b848',
}
fake_transfer = {
"id": "f21c72c4-2b77-445b-aa12-e8d1b44163a2",
"created_at": "2022-09-06T08:17:43.629495",
"name": "test_transfer",
"resource_type": "share",
"resource_id": "29476819-28a9-4b1a-a21d-3b2d203025a0",
"auth_key": "406a2d67cdb09afe",
"source_project_id": "714198c7ac5e45a4b785de732ea4695d",
"destination_project_id": None,
"accepted": False,
"expires_at": None,
}
def get_messages(self, **kw):
messages = {
'messages': [self.fake_message],
@ -1333,6 +1346,25 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
def delete_messages_5678(self, **kw):
return 202, {}, None
def post_share_transfers(self, **kw):
transfer = {'transfer': self.fake_transfer}
return 202, {}, transfer
def get_share_transfers_5678(self, **kw):
transfer = {'transfer': self.fake_transfer}
return 202, {}, transfer
def get_share_transfers_detail(self, **kw):
transfer = {'transfers': [self.fake_transfer]}
return 202, {}, transfer
def delete_share_transfers_5678(self, **kw):
return 202, {}, None
def post_share_transfers_5678_accept(self, **kw):
transfer = {'transfer': self.fake_transfer}
return 202, {}, transfer
def fake_create(url, body, response_key):
return {'url': url, 'body': body, 'resp_key': response_key}

View File

@ -0,0 +1,50 @@
# Copyright (c) 2022 China Telecom Digital Intelligence.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from manilaclient import api_versions
from manilaclient.tests.unit import utils
from manilaclient.tests.unit.v2 import fakes
TRANSFER_URL = 'share-transfers'
cs = fakes.FakeClient(api_versions.APIVersion('2.77'))
class ShareTransfersTest(utils.TestCase):
def test_create(self):
cs.transfers.create('1234')
cs.assert_called('POST', '/%s' % TRANSFER_URL,
body={'transfer': {'share_id': '1234',
'name': None}})
def test_get(self):
transfer_id = '5678'
cs.transfers.get(transfer_id)
cs.assert_called('GET', '/%s/%s' % (TRANSFER_URL, transfer_id))
def test_list(self):
cs.transfers.list()
cs.assert_called('GET', '/%s/detail' % TRANSFER_URL)
def test_delete(self):
cs.transfers.delete('5678')
cs.assert_called('DELETE', '/%s/5678' % TRANSFER_URL)
def test_accept(self):
transfer_id = '5678'
auth_key = '12345'
cs.transfers.accept(transfer_id, auth_key)
cs.assert_called('POST',
'/%s/%s/accept' % (TRANSFER_URL, transfer_id))

View File

@ -43,6 +43,7 @@ from manilaclient.v2 import share_snapshot_export_locations
from manilaclient.v2 import share_snapshot_instance_export_locations
from manilaclient.v2 import share_snapshot_instances
from manilaclient.v2 import share_snapshots
from manilaclient.v2 import share_transfers
from manilaclient.v2 import share_type_access
from manilaclient.v2 import share_types
from manilaclient.v2 import shares
@ -189,6 +190,7 @@ class Client(object):
self.availability_zones = availability_zones.AvailabilityZoneManager(
self)
self.limits = limits.LimitsManager(self)
self.transfers = share_transfers.ShareTransferManager(self)
self.messages = messages.MessageManager(self)
self.services = services.ServiceManager(self)
self.security_services = security_services.SecurityServiceManager(self)

View File

@ -0,0 +1,121 @@
# Copyright (c) 2022 China Telecom Digital Intelligence.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from manilaclient import api_versions
from manilaclient import base
from manilaclient.common import constants
class ShareTransfer(base.Resource):
"""Transfer a share from one tenant to another"""
def __repr__(self):
return "<ShareTransfer: %s>" % self.id
def delete(self):
"""Delete this share transfer."""
return self.manager.delete(self)
class ShareTransferManager(base.ManagerWithFind):
"""Manage :class:`ShareTransfer` resources."""
resource_class = ShareTransfer
@api_versions.wraps(constants.SHARE_TRANSFER_VERSION)
def create(self, share_id, name=None):
"""Creates a share transfer.
:param share_id: The ID of the share to transfer.
:param name: The name of the transfer.
:rtype: :class:`ShareTransfer`
"""
body = {'transfer': {'share_id': share_id,
'name': name}}
return self._create('/share-transfers', body, 'transfer')
@api_versions.wraps(constants.SHARE_TRANSFER_VERSION)
def accept(self, transfer, auth_key, clear_access_rules=False):
"""Accept a share transfer.
:param transfer_id: The ID of the transfer to accept.
:param auth_key: The auth_key of the transfer.
:param clear_access_rules: Transfer share without access rules
:rtype: :class:`ShareTransfer`
"""
transfer_id = base.getid(transfer)
body = {'accept': {'auth_key': auth_key,
'clear_access_rules': clear_access_rules}}
self._accept('/share-transfers/%s/accept' % transfer_id, body)
@api_versions.wraps(constants.SHARE_TRANSFER_VERSION)
def get(self, transfer_id):
"""Show details of a share transfer.
:param transfer_id: The ID of the share transfer to display.
:rtype: :class:`ShareTransfer`
"""
return self._get("/share-transfers/%s" % transfer_id, "transfer")
@api_versions.wraps(constants.SHARE_TRANSFER_VERSION)
def list(self, detailed=True, search_opts=None,
sort_key=None, sort_dir=None):
"""Get a list of all share transfer.
:param detailed: Get detailed object information.
:param search_opts: Filtering options.
:param sort_key: Key to be sorted (i.e. 'created_at').
:param sort_dir: Sort direction, should be 'desc' or 'asc'.
:rtype: list of :class:`ShareTransfer`
"""
if search_opts is None:
search_opts = {}
if sort_key is not None:
if sort_key in constants.SHARE_TRANSFER_SORT_KEY_VALUES:
search_opts['sort_key'] = sort_key
# NOTE: Replace aliases with appropriate keys
if sort_key == 'name':
search_opts['sort_key'] = 'display_name'
else:
raise ValueError(
'sort_key must be one of the following: %s.'
% ', '.join(constants.SHARE_TRANSFER_SORT_KEY_VALUES))
if sort_dir is not None:
if sort_dir in constants.SORT_DIR_VALUES:
search_opts['sort_dir'] = sort_dir
else:
raise ValueError('sort_dir must be one of the following: %s.'
% ', '.join(constants.SORT_DIR_VALUES))
query_string = self._build_query_string(search_opts)
if detailed:
path = "/share-transfers/detail%s" % (query_string,)
else:
path = "/share-transfers%s" % (query_string,)
return self._list(path, 'transfers')
@api_versions.wraps(constants.SHARE_TRANSFER_VERSION)
def delete(self, transfer_id):
"""Delete a share transfer.
:param transfer_id: The :class:`ShareTransfer` to delete.
"""
return self._delete("/share-transfers/%s" % base.getid(transfer_id))

View File

@ -125,6 +125,11 @@ def _find_share(cs, share):
return apiclient_utils.find_resource(cs.shares, share)
def _find_share_transfer(cs, transfer):
"""Get a share transfer by ID."""
return apiclient_utils.find_resource(cs.transfers, transfer)
@api_versions.wraps("1.0", "2.8")
def _print_share(cs, share):
info = share._info.copy()
@ -6532,6 +6537,212 @@ def do_share_replica_resync(cs, args):
cs.share_replicas.resync(replica)
##############################################################################
#
# Share Transfer
#
##############################################################################
def _print_share_transfer(transfer):
info = transfer._info.copy()
info.pop('links', None)
cliutils.print_dict(info)
@api_versions.wraps("2.77")
@cliutils.arg(
'share',
metavar='<share>',
help='Name or ID of share to transfer.')
@cliutils.arg(
'--name',
metavar='<name>',
default=None,
help='Transfer name. Default=None.')
def do_share_transfer_create(cs, args):
"""Creates a share transfer."""
share = _find_share(cs, args.share)
transfer = cs.transfers.create(share.id,
args.name)
_print_share_transfer(transfer)
@api_versions.wraps("2.77")
@cliutils.arg(
'transfer',
metavar='<transfer>',
nargs='+',
help='ID or name of the transfer(s).')
def do_share_transfer_delete(cs, args):
"""Remove one or more transfers."""
failure_count = 0
for transfer in args.transfer:
try:
transfer_ref = _find_share_transfer(cs, transfer)
transfer_ref.delete()
except Exception as e:
failure_count += 1
print("Delete for share transfer %s failed: %s" % (transfer, e),
file=sys.stderr)
if failure_count == len(args.transfer):
raise exceptions.CommandError("Unable to delete any of the specified "
"transfers.")
@api_versions.wraps("2.77")
@cliutils.arg(
'transfer',
metavar='<transfer>',
help='ID of transfer to accept.')
@cliutils.arg(
'auth_key',
metavar='<auth_key>',
help='Authentication key of transfer to accept.')
@cliutils.arg(
'--clear-rules',
'--clear_rules',
dest='clear_rules',
action='store_true',
default=False,
help="Whether manila should clean up the access rules after the "
"transfer is complete. (Default=False)")
def do_share_transfer_accept(cs, args):
"""Accepts a share transfer."""
cs.transfers.accept(args.transfer, args.auth_key,
clear_access_rules=args.clear_rules)
@api_versions.wraps("2.77")
@cliutils.arg(
'--all-tenants', '--all-projects',
action='single_alias',
dest='all_projects',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help='Shows details for all tenants. (Admin only).')
@cliutils.arg(
'--name',
metavar='<name>',
default=None,
action='single_alias',
help='Transfer name. Default=None.')
@cliutils.arg(
'--id',
metavar='<id>',
default=None,
action='single_alias',
help='Transfer ID. Default=None.')
@cliutils.arg(
'--resource-type', '--resource_type',
metavar='<resource_type>',
default=None,
action='single_alias',
help='Transfer type, which can be share or network. Default=None.')
@cliutils.arg(
'--resource-id', '--resource_id',
metavar='<resource_id>',
default=None,
action='single_alias',
help='Transfer resource id. Default=None.')
@cliutils.arg(
'--source-project-id', '--source_project_id',
metavar='<source_project_id>',
default=None,
action='single_alias',
help='Transfer source project id. Default=None.')
@cliutils.arg(
'--limit',
metavar='<limit>',
type=int,
default=None,
help='Maximum number of messages to return. (Default=None)')
@cliutils.arg(
'--offset',
metavar="<offset>",
default=None,
help='Start position of message listing.')
@cliutils.arg(
'--sort-key', '--sort_key',
metavar='<sort_key>',
type=str,
default=None,
action='single_alias',
help='Key to be sorted, available keys are %(keys)s. Default=None.'
% {'keys': constants.SHARE_TRANSFER_SORT_KEY_VALUES})
@cliutils.arg(
'--sort-dir', '--sort_dir',
metavar='<sort_dir>',
type=str,
default=None,
action='single_alias',
help='Sort direction, available values are %(values)s. '
'Optional: Default=None.' % {'values': constants.SORT_DIR_VALUES})
@cliutils.arg(
'--detailed',
dest='detailed',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help="Show detailed information about filtered share transfers.")
@cliutils.arg(
'--columns',
metavar='<columns>',
type=str,
default=None,
help='Comma separated list of columns to be displayed '
'example --columns "id,resource_id".')
def do_share_transfer_list(cs, args):
"""Lists all transfers."""
if args.columns is not None:
list_of_keys = _split_columns(columns=args.columns)
else:
list_of_keys = ['ID', 'Name', 'Resource Type', 'Resource Id']
if args.detailed:
list_of_keys.extend(['Created At', 'Expires At', 'Source Project Id',
'Destination Project Id', 'Accepted'])
all_projects = int(
os.environ.get("ALL_TENANTS",
os.environ.get("ALL_PROJECTS",
args.all_projects))
)
search_opts = {
'offset': args.offset,
'limit': args.limit,
'all_tenants': all_projects,
'id': args.id,
'name': args.name,
'resource_type': args.resource_type,
'resource_id': args.resource_id,
'source_project_id': args.source_project_id,
}
share_transfers = cs.transfers.list(
detailed=args.detailed, search_opts=search_opts,
sort_key=args.sort_key, sort_dir=args.sort_dir)
cliutils.print_list(share_transfers, fields=list_of_keys,
sortby_index=None)
@api_versions.wraps("2.77")
@cliutils.arg(
'transfer',
metavar='<transfer>',
help='Name or ID of transfer to show.')
def do_share_transfer_show(cs, args):
"""Delete a transfer."""
transfer = _find_share_transfer(cs, args.transfer)
_print_share_transfer(transfer)
##############################################################################
#
# User Messages

View File

@ -0,0 +1,3 @@
---
features:
- Support transferring shares between projects starting from API version ``2.77``.

View File

@ -159,6 +159,11 @@ openstack.share.v2 =
share_server_migration_complete = manilaclient.osc.v2.share_servers:ShareServerMigrationComplete
share_server_migration_show = manilaclient.osc.v2.share_servers:ShareServerMigrationShow
share_server_migration_start = manilaclient.osc.v2.share_servers:ShareServerMigrationStart
share_transfer_create = manilaclient.osc.v2.share_transfers:CreateShareTransfer
share_transfer_delete = manilaclient.osc.v2.share_transfers:DeleteShareTransfer
share_transfer_list = manilaclient.osc.v2.share_transfers:ListShareTransfer
share_transfer_show = manilaclient.osc.v2.share_transfers:ShowShareTransfer
share_transfer_accept = manilaclient.osc.v2.share_transfers:AcceptShareTransfer
[coverage:run]
omit = manilaclient/tests/*