Merge "Transfer snapshots with volumes"

This commit is contained in:
Zuul 2018-07-19 17:09:09 +00:00 committed by Gerrit Code Review
commit 4bb00dbcf6
27 changed files with 1014 additions and 28 deletions

View File

@ -1925,6 +1925,13 @@ new_type:
in: body in: body
required: true required: true
type: string type: string
no_snapshots:
description: |
Transfer volume without snapshots.
in: body
required: false
min_version: 3.55
type: boolean
object_count: object_count:
description: | description: |
The number of objects in the backup. The number of objects in the backup.

View File

@ -1,6 +1,7 @@
{ {
"transfer": { "transfer": {
"volume_id": "c86b9af4-151d-4ead-b62c-5fb967af0e37", "volume_id": "c86b9af4-151d-4ead-b62c-5fb967af0e37",
"name": "first volume" "name": "first volume",
"no_snapshots": False,
} }
} }

View File

@ -0,0 +1,268 @@
.. -*- rst -*-
Volume transfer
===============
Transfers a volume from one user to another user.
This is the new transfer APIs with microversion 3.53.
Accept a volume transfer
~~~~~~~~~~~~~~~~~~~~~~~~
.. rest_method:: POST /v3/{project_id}/volume_transfers/{transfer_id}/accept
Accepts a volume transfer.
Response codes
--------------
.. rest_status_code:: success ../status.yaml
- 202
.. rest_status_code:: error ../status.yaml
- 400
.. rest_status_code:: error ../status.yaml
- 413
Request
-------
.. rest_parameters:: parameters.yaml
- project_id: project_id_path
- transfer_id: transfer_id
- auth_key: auth_key
Request Example
---------------
.. literalinclude:: ./samples/volume-transfer-accept-request.json
:language: javascript
Response Parameters
-------------------
.. rest_parameters:: parameters.yaml
- transfer: transfer
- volume_id: volume_id
- id: id
- links: links
- name: name
Response Example
----------------
.. literalinclude:: ./samples/volume-transfer-accept-response.json
:language: javascript
Create a volume transfer
~~~~~~~~~~~~~~~~~~~~~~~~
.. rest_method:: POST /v3/{project_id}/volume_transfers
Creates a volume transfer.
Response codes
--------------
.. rest_status_code:: success ../status.yaml
- 202
.. rest_status_code:: error ../status.yaml
- 400
Request
-------
.. rest_parameters:: parameters.yaml
- project_id: project_id_path
- transfer: transfer
- name: name
- volume_id: volume_id
- no_snapshots: no_snapshots
Request Example
---------------
.. literalinclude:: ./samples/volume-transfer-create-request.json
:language: javascript
Response Parameters
-------------------
.. rest_parameters:: parameters.yaml
- auth_key: auth_key
- links: links
- created_at: created_at
- volume_id: volume_id
- id: id
- name: name
Response Example
----------------
.. literalinclude:: ./samples/volume-transfer-create-response.json
:language: javascript
List volume transfers for a project
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. rest_method:: GET /v3/{project_id}/volume_transfers
Lists volume transfers.
Response codes
--------------
.. rest_status_code:: success ../status.yaml
- 200
Request
-------
.. rest_parameters:: parameters.yaml
- project_id: project_id_path
- all_tenants: all-tenants
Response Parameters
-------------------
.. rest_parameters:: parameters.yaml
- volume_id: volume_id
- id: id
- links: links
- name: name
Response Example
----------------
.. literalinclude:: ./samples/volume-transfers-list-response.json
:language: javascript
Show volume transfer detail
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. rest_method:: GET /v3/{project_id}/volume_transfers/{transfer_id}
Shows details for a volume transfer.
Response codes
--------------
.. rest_status_code:: success ../status.yaml
- 200
Request
-------
.. rest_parameters:: parameters.yaml
- project_id: project_id_path
- transfer_id: transfer_id
Response Parameters
-------------------
.. rest_parameters:: parameters.yaml
- created_at: created_at
- volume_id: volume_id
- id: id
- links: links
- name: name
Response Example
----------------
.. literalinclude:: ./samples/volume-transfer-show-response.json
:language: javascript
Delete a volume transfer
~~~~~~~~~~~~~~~~~~~~~~~~
.. rest_method:: DELETE /v3/{project_id}/volume_transfers/{transfer_id}
Deletes a volume transfer.
Response codes
--------------
.. rest_status_code:: success ../status.yaml
- 202
Request
-------
.. rest_parameters:: parameters.yaml
- project_id: project_id_path
- transfer_id: transfer_id
List volume transfers and details
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. rest_method:: GET /v3/{project_id}/volume_transfers/detail
Lists volume transfers, with details.
Response codes
--------------
.. rest_status_code:: success ../status.yaml
- 200
Request
-------
.. rest_parameters:: parameters.yaml
- project_id: project_id_path
- all_tenants: all-tenants
Response Parameters
-------------------
.. rest_parameters:: parameters.yaml
- transfers: transfers
- created_at: created_at
- volume_id: volume_id
- id: id
- links: links
- name: name
Response Example
----------------
.. literalinclude:: ./samples/volume-transfers-list-detailed-response.json
:language: javascript

View File

@ -93,7 +93,8 @@ class VolumeTransferController(wsgi.Controller):
volume_id) volume_id)
try: try:
new_transfer = self.transfer_api.create(context, volume_id, name) new_transfer = self.transfer_api.create(context, volume_id, name,
no_snapshots=False)
# Not found exception will be handled at the wsgi level # Not found exception will be handled at the wsgi level
except exception.InvalidVolume as error: except exception.InvalidVolume as error:
raise exc.HTTPBadRequest(explanation=error.msg) raise exc.HTTPBadRequest(explanation=error.msg)

View File

@ -147,6 +147,8 @@ SUPPORT_VOLUME_SCHEMA_CHANGES = '3.53'
ATTACHMENT_CREATE_MODE_ARG = '3.54' ATTACHMENT_CREATE_MODE_ARG = '3.54'
TRANSFER_WITH_SNAPSHOTS = '3.55'
def get_mv_header(version): def get_mv_header(version):
"""Gets a formatted HTTP microversion header. """Gets a formatted HTTP microversion header.

View File

@ -126,6 +126,7 @@ REST_API_VERSION_HISTORY = """
parameter in the request body in order to update the volume. parameter in the request body in order to update the volume.
Also, additional parameters will not be allowed. Also, additional parameters will not be allowed.
* 3.54 - Add ``mode`` argument to attachment-create. * 3.54 - Add ``mode`` argument to attachment-create.
* 3.55 - Support transfer volume with snapshots
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
@ -133,7 +134,7 @@ REST_API_VERSION_HISTORY = """
# minimum version of the API supported. # minimum version of the API supported.
# Explicitly using /v2 endpoints will still work # Explicitly using /v2 endpoints will still work
_MIN_API_VERSION = "3.0" _MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.54" _MAX_API_VERSION = "3.55"
_LEGACY_API_VERSION2 = "2.0" _LEGACY_API_VERSION2 = "2.0"
UPDATED = "2018-07-17T00:00:00Z" UPDATED = "2018-07-17T00:00:00Z"

View File

@ -437,3 +437,7 @@ volume APIs.
3.54 3.54
---- ----
Add ``mode`` argument to attachment-create. Add ``mode`` argument to attachment-create.
3.55
----
Support ability to transfer snapshots along with their parent volume.

View File

@ -30,7 +30,7 @@ create = {
'name': {'oneOf': [{'type': 'string', 'name': {'oneOf': [{'type': 'string',
'format': 'format':
"name_skip_leading_trailing_spaces"}, "name_skip_leading_trailing_spaces"},
{'type': 'null'}]} {'type': 'null'}]},
}, },
'required': ['volume_id'], 'required': ['volume_id'],
'additionalProperties': False, 'additionalProperties': False,
@ -56,3 +56,25 @@ accept = {
'required': ['accept'], 'required': ['accept'],
'additionalProperties': False, 'additionalProperties': False,
} }
create_v355 = {
'type': 'object',
'properties': {
'transfer': {
'type': 'object',
'properties': {
'volume_id': parameter_types.uuid,
'name': {'oneOf': [{'type': 'string',
'format':
"name_skip_leading_trailing_spaces"},
{'type': 'null'}]},
'no_snapshots': parameter_types.boolean
},
'required': ['volume_id'],
'additionalProperties': False,
},
},
'required': ['transfer'],
'additionalProperties': False,
}

View File

@ -38,6 +38,7 @@ from cinder.api.v3 import snapshot_manage
from cinder.api.v3 import snapshots from cinder.api.v3 import snapshots
from cinder.api.v3 import volume_manage from cinder.api.v3 import volume_manage
from cinder.api.v3 import volume_metadata from cinder.api.v3 import volume_metadata
from cinder.api.v3 import volume_transfer
from cinder.api.v3 import volumes from cinder.api.v3 import volumes
from cinder.api.v3 import workers from cinder.api.v3 import workers
from cinder.api import versions from cinder.api import versions
@ -192,3 +193,10 @@ class APIRouter(cinder.api.openstack.APIRouter):
ext_mgr) ext_mgr)
mapper.resource('resource_filter', 'resource_filters', mapper.resource('resource_filter', 'resource_filters',
controller=self.resources['resource_filters']) controller=self.resources['resource_filters'])
self.resources['volume_transfers'] = (
volume_transfer.create_resource())
mapper.resource("volume_transfer", "volume_transfers",
controller=self.resources['volume_transfers'],
collection={'detail': 'GET'},
member={'accept': 'POST'})

View File

@ -0,0 +1,66 @@
# Copyright 2018 FiberHome Telecommunication Technologies CO.,LTD
#
# 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 oslo_log import log as logging
from oslo_utils import strutils
from six.moves import http_client
from webob import exc
from cinder.api.contrib import volume_transfer as volume_transfer_v2
from cinder.api.openstack import wsgi
from cinder.api.schemas import volume_transfer
from cinder.api import validation
from cinder import exception
LOG = logging.getLogger(__name__)
class VolumeTransferController(volume_transfer_v2.VolumeTransferController):
"""The transfer API controller for the OpenStack API V3."""
@wsgi.response(http_client.ACCEPTED)
@validation.schema(volume_transfer.create, '3.0', '3.54')
@validation.schema(volume_transfer.create_v355, '3.55')
def create(self, req, body):
"""Create a new volume transfer."""
LOG.debug('Creating new volume transfer %s', body)
context = req.environ['cinder.context']
transfer = body['transfer']
volume_id = transfer['volume_id']
name = transfer.get('name', None)
if name is not None:
name = name.strip()
no_snapshots = strutils.bool_from_string(transfer.get('no_snapshots',
False))
LOG.info("Creating transfer of volume %s", volume_id)
try:
new_transfer = self.transfer_api.create(context, volume_id, name,
no_snapshots=no_snapshots)
# Not found exception will be handled at the wsgi level
except exception.Invalid as error:
raise exc.HTTPBadRequest(explanation=error.msg)
transfer = self._view_builder.create(req,
dict(new_transfer))
return transfer
def create_resource():
return wsgi.Resource(VolumeTransferController())

View File

@ -14,6 +14,7 @@
# under the License. # under the License.
from cinder.api import common from cinder.api import common
from cinder.api import microversions as mv
class ViewBuilder(common.ViewBuilder): class ViewBuilder(common.ViewBuilder):
@ -49,7 +50,7 @@ class ViewBuilder(common.ViewBuilder):
def detail(self, request, transfer): def detail(self, request, transfer):
"""Detailed view of a single transfer.""" """Detailed view of a single transfer."""
return { detail_body = {
'transfer': { 'transfer': {
'id': transfer.get('id'), 'id': transfer.get('id'),
'created_at': transfer.get('created_at'), 'created_at': transfer.get('created_at'),
@ -58,10 +59,15 @@ class ViewBuilder(common.ViewBuilder):
'links': self._get_links(request, transfer['id']) 'links': self._get_links(request, transfer['id'])
} }
} }
req_version = request.api_version_request
if req_version.matches(mv.TRANSFER_WITH_SNAPSHOTS):
detail_body['transfer'].update({'no_snapshots':
transfer.get('no_snapshots')})
return detail_body
def create(self, request, transfer): def create(self, request, transfer):
"""Detailed view of a single transfer when created.""" """Detailed view of a single transfer when created."""
return { create_body = {
'transfer': { 'transfer': {
'id': transfer.get('id'), 'id': transfer.get('id'),
'created_at': transfer.get('created_at'), 'created_at': transfer.get('created_at'),
@ -71,6 +77,11 @@ class ViewBuilder(common.ViewBuilder):
'links': self._get_links(request, transfer['id']) 'links': self._get_links(request, transfer['id'])
} }
} }
req_version = request.api_version_request
if req_version.matches(mv.TRANSFER_WITH_SNAPSHOTS):
create_body['transfer'].update({'no_snapshots':
transfer.get('no_snapshots')})
return create_body
def _list_view(self, func, request, transfers, origin_transfer_count): def _list_view(self, func, request, transfers, origin_transfer_count):
"""Provide a view for a list of transfers.""" """Provide a view for a list of transfers."""

View File

@ -1284,9 +1284,11 @@ def transfer_destroy(context, transfer_id):
return IMPL.transfer_destroy(context, transfer_id) return IMPL.transfer_destroy(context, transfer_id)
def transfer_accept(context, transfer_id, user_id, project_id): def transfer_accept(context, transfer_id, user_id, project_id,
no_snapshots=False):
"""Accept a volume transfer.""" """Accept a volume transfer."""
return IMPL.transfer_accept(context, transfer_id, user_id, project_id) return IMPL.transfer_accept(context, transfer_id, user_id, project_id,
no_snapshots=no_snapshots)
################### ###################

View File

@ -5417,7 +5417,8 @@ def transfer_get(context, transfer_id):
def _translate_transfers(transfers): def _translate_transfers(transfers):
fields = ('id', 'volume_id', 'display_name', 'created_at', 'deleted') fields = ('id', 'volume_id', 'display_name', 'created_at', 'deleted',
'no_snapshots')
return [{k: transfer[k] for k in fields} for transfer in transfers] return [{k: transfer[k] for k in fields} for transfer in transfers]
@ -5491,8 +5492,37 @@ def transfer_destroy(context, transfer_id):
return updated_values return updated_values
def _roll_back_transferred_volume_and_snapshots(context, volume_id,
old_user_id, old_project_id,
transffered_snapshots):
expected = {'id': volume_id, 'status': 'available'}
update = {'status': 'awaiting-transfer',
'user_id': old_user_id,
'project_id': old_project_id,
'updated_at': timeutils.utcnow()}
if not conditional_update(context, models.Volume, update, expected):
LOG.warning('Volume: %(volume_id)s is not in the expected available '
'status. Rolling it back.', {'volume_id': volume_id})
return
for snapshot_id in transffered_snapshots:
LOG.info('Beginning to roll back transferred snapshots: %s',
snapshot_id)
expected = {'id': snapshot_id,
'status': 'available'}
update = {'user_id': old_user_id,
'project_id': old_project_id,
'updated_at': timeutils.utcnow()}
if not conditional_update(context, models.Snapshot, update, expected):
LOG.warning('Snapshot: %(snapshot_id)s is not in the expected '
'available state. Rolling it back.',
{'snapshot_id': snapshot_id})
return
@require_context @require_context
def transfer_accept(context, transfer_id, user_id, project_id): def transfer_accept(context, transfer_id, user_id, project_id,
no_snapshots=False):
session = get_session() session = get_session()
with session.begin(): with session.begin():
volume_id = _transfer_get(context, transfer_id, session)['volume_id'] volume_id = _transfer_get(context, transfer_id, session)['volume_id']
@ -5500,7 +5530,8 @@ def transfer_accept(context, transfer_id, user_id, project_id):
'status': 'awaiting-transfer'} 'status': 'awaiting-transfer'}
update = {'status': 'available', update = {'status': 'available',
'user_id': user_id, 'user_id': user_id,
'project_id': project_id} 'project_id': project_id,
'updated_at': timeutils.utcnow()}
if not conditional_update(context, models.Volume, update, expected): if not conditional_update(context, models.Volume, update, expected):
msg = (_('Transfer %(transfer_id)s: Volume id %(volume_id)s ' msg = (_('Transfer %(transfer_id)s: Volume id %(volume_id)s '
'expected in awaiting-transfer state.') 'expected in awaiting-transfer state.')
@ -5508,6 +5539,33 @@ def transfer_accept(context, transfer_id, user_id, project_id):
LOG.error(msg) LOG.error(msg)
raise exception.InvalidVolume(reason=msg) raise exception.InvalidVolume(reason=msg)
# Update snapshots for transfer snapshots with volume.
if not no_snapshots:
snapshots = snapshot_get_all_for_volume(context, volume_id)
transferred_snapshots = []
for snapshot in snapshots:
LOG.info('Begin to transfer snapshot: %s', snapshot['id'])
old_user_id = snapshot['user_id']
old_project_id = snapshot['project_id']
expected = {'id': snapshot['id'],
'status': 'available'}
update = {'user_id': user_id,
'project_id': project_id,
'updated_at': timeutils.utcnow()}
if not conditional_update(context, models.Snapshot, update,
expected):
msg = (_('Transfer %(transfer_id)s: Snapshot '
'%(snapshot_id)s is not in the expected '
'available state.')
% {'transfer_id': transfer_id,
'snapshot_id': snapshot['id']})
LOG.warning(msg)
_roll_back_transferred_volume_and_snapshots(
context, volume_id, old_user_id, old_project_id,
transferred_snapshots)
raise exception.InvalidSnapshot(reason=msg)
transferred_snapshots.append(snapshot['id'])
(session.query(models.Transfer) (session.query(models.Transfer)
.filter_by(id=transfer_id) .filter_by(id=transfer_id)
.update({'deleted': True, .update({'deleted': True,

View File

@ -0,0 +1,21 @@
# 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 sqlalchemy import Boolean, Column, MetaData, Table
def upgrade(migrate_engine):
"""Add the no_snapshots column to the transfers table."""
meta = MetaData(bind=migrate_engine)
transfers = Table('transfers', meta, autoload=True)
if not hasattr(transfers.c, 'no_snapshots'):
transfers.create_column(Column('no_snapshots', Boolean, default=False))

View File

@ -830,6 +830,7 @@ class Transfer(BASE, CinderBase):
salt = Column(String(255)) salt = Column(String(255))
crypt_hash = Column(String(255)) crypt_hash = Column(String(255))
expires_at = Column(DateTime) expires_at = Column(DateTime)
no_snapshots = Column(Boolean, default=False)
volume = relationship(Volume, backref="transfer", volume = relationship(Volume, backref="transfer",
foreign_keys=volume_id, foreign_keys=volume_id,
primaryjoin='and_(' primaryjoin='and_('

View File

@ -38,6 +38,14 @@ volume_transfer_policies = [
{ {
'method': 'GET', 'method': 'GET',
'path': '/os-volume-transfer/detail' 'path': '/os-volume-transfer/detail'
},
{
'method': 'GET',
'path': '/volume_transfers'
},
{
'method': 'GET',
'path': '/volume_transfers/detail'
} }
]), ]),
policy.DocumentedRuleDefault( policy.DocumentedRuleDefault(
@ -48,6 +56,10 @@ volume_transfer_policies = [
{ {
'method': 'POST', 'method': 'POST',
'path': '/os-volume-transfer' 'path': '/os-volume-transfer'
},
{
'method': 'POST',
'path': '/volume_transfers'
} }
]), ]),
policy.DocumentedRuleDefault( policy.DocumentedRuleDefault(
@ -58,6 +70,10 @@ volume_transfer_policies = [
{ {
'method': 'GET', 'method': 'GET',
'path': '/os-volume-transfer/{transfer_id}' 'path': '/os-volume-transfer/{transfer_id}'
},
{
'method': 'GET',
'path': '/volume_transfers/{transfer_id}'
} }
]), ]),
policy.DocumentedRuleDefault( policy.DocumentedRuleDefault(
@ -68,6 +84,10 @@ volume_transfer_policies = [
{ {
'method': 'POST', 'method': 'POST',
'path': '/os-volume-transfer/{transfer_id}/accept' 'path': '/os-volume-transfer/{transfer_id}/accept'
},
{
'method': 'POST',
'path': '/volume_transfers/{transfer_id}/accept'
} }
]), ]),
policy.DocumentedRuleDefault( policy.DocumentedRuleDefault(
@ -78,6 +98,10 @@ volume_transfer_policies = [
{ {
'method': 'DELETE', 'method': 'DELETE',
'path': '/os-volume-transfer/{transfer_id}' 'path': '/os-volume-transfer/{transfer_id}'
},
{
'method': 'DELETE',
'path': '/volume_transfers/{transfer_id}'
} }
]), ]),
] ]

View File

@ -0,0 +1,292 @@
# Copyright 2018 FiberHome Telecommunication Technologies CO.,LTD
# 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.
"""
Tests for volume transfer code.
"""
from oslo_serialization import jsonutils
from six.moves import http_client
import webob
from cinder.api.contrib import volume_transfer
from cinder.api import microversions as mv
from cinder import context
from cinder import db
from cinder.objects import fields
from cinder import test
from cinder.tests.unit.api import fakes
from cinder.tests.unit import fake_constants as fake
import cinder.transfer
class VolumeTransferAPITestCase(test.TestCase):
"""Test Case for transfers V3 API."""
def setUp(self):
super(VolumeTransferAPITestCase, self).setUp()
self.volume_transfer_api = cinder.transfer.API()
self.controller = volume_transfer.VolumeTransferController()
self.user_ctxt = context.RequestContext(
fake.USER_ID, fake.PROJECT_ID, auth_token=True, is_admin=True)
def _create_transfer(self, volume_id=fake.VOLUME_ID,
display_name='test_transfer'):
"""Create a transfer object."""
return self.volume_transfer_api.create(context.get_admin_context(),
volume_id,
display_name)
@staticmethod
def _create_volume(display_name='test_volume',
display_description='this is a test volume',
status='available',
size=1,
project_id=fake.PROJECT_ID,
attach_status=fields.VolumeAttachStatus.DETACHED):
"""Create a volume object."""
vol = {}
vol['host'] = 'fake_host'
vol['size'] = size
vol['user_id'] = fake.USER_ID
vol['project_id'] = project_id
vol['status'] = status
vol['display_name'] = display_name
vol['display_description'] = display_description
vol['attach_status'] = attach_status
vol['availability_zone'] = 'fake_zone'
return db.volume_create(context.get_admin_context(), vol)['id']
def test_show_transfer(self):
volume_id = self._create_volume(size=5)
transfer = self._create_transfer(volume_id)
req = webob.Request.blank('/v3/%s/volume_transfers/%s' % (
fake.PROJECT_ID, transfer['id']))
req.method = 'GET'
req.headers = mv.get_mv_header(mv.TRANSFER_WITH_SNAPSHOTS)
req.headers['Content-Type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_ctxt))
res_dict = jsonutils.loads(res.body)
self.assertEqual(http_client.OK, res.status_int)
self.assertEqual('test_transfer', res_dict['transfer']['name'])
self.assertEqual(transfer['id'], res_dict['transfer']['id'])
self.assertEqual(volume_id, res_dict['transfer']['volume_id'])
db.transfer_destroy(context.get_admin_context(), transfer['id'])
db.volume_destroy(context.get_admin_context(), volume_id)
def test_list_transfers_json(self):
volume_id_1 = self._create_volume(size=5)
volume_id_2 = self._create_volume(size=5)
transfer1 = self._create_transfer(volume_id_1)
transfer2 = self._create_transfer(volume_id_2)
req = webob.Request.blank('/v3/%s/volume_transfers' %
fake.PROJECT_ID)
req.method = 'GET'
req.headers = mv.get_mv_header(mv.TRANSFER_WITH_SNAPSHOTS)
req.headers['Content-Type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_ctxt))
res_dict = jsonutils.loads(res.body)
self.assertEqual(http_client.OK, res.status_int)
self.assertEqual(4, len(res_dict['transfers'][0]))
self.assertEqual(transfer1['id'], res_dict['transfers'][0]['id'])
self.assertEqual('test_transfer', res_dict['transfers'][0]['name'])
self.assertEqual(4, len(res_dict['transfers'][1]))
self.assertEqual('test_transfer', res_dict['transfers'][1]['name'])
db.transfer_destroy(context.get_admin_context(), transfer2['id'])
db.transfer_destroy(context.get_admin_context(), transfer1['id'])
db.volume_destroy(context.get_admin_context(), volume_id_1)
db.volume_destroy(context.get_admin_context(), volume_id_2)
def test_list_transfers_detail_json(self):
volume_id_1 = self._create_volume(size=5)
volume_id_2 = self._create_volume(size=5)
transfer1 = self._create_transfer(volume_id_1)
transfer2 = self._create_transfer(volume_id_2)
req = webob.Request.blank('/v3/%s/volume_transfers/detail' %
fake.PROJECT_ID)
req.method = 'GET'
req.headers = mv.get_mv_header(mv.TRANSFER_WITH_SNAPSHOTS)
req.headers['Content-Type'] = 'application/json'
req.headers['Accept'] = 'application/json'
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_ctxt))
res_dict = jsonutils.loads(res.body)
self.assertEqual(http_client.OK, res.status_int)
self.assertEqual(6, len(res_dict['transfers'][0]))
self.assertEqual('test_transfer',
res_dict['transfers'][0]['name'])
self.assertEqual(transfer1['id'], res_dict['transfers'][0]['id'])
self.assertEqual(volume_id_1, res_dict['transfers'][0]['volume_id'])
self.assertEqual(6, len(res_dict['transfers'][1]))
self.assertEqual('test_transfer',
res_dict['transfers'][1]['name'])
self.assertEqual(transfer2['id'], res_dict['transfers'][1]['id'])
self.assertEqual(volume_id_2, res_dict['transfers'][1]['volume_id'])
db.transfer_destroy(context.get_admin_context(), transfer2['id'])
db.transfer_destroy(context.get_admin_context(), transfer1['id'])
db.volume_destroy(context.get_admin_context(), volume_id_2)
db.volume_destroy(context.get_admin_context(), volume_id_1)
def test_list_transfers_detail_json_with_no_snapshots(self):
volume_id_1 = self._create_volume(size=5)
volume_id_2 = self._create_volume(size=5)
transfer1 = self._create_transfer(volume_id_1)
transfer2 = self._create_transfer(volume_id_2)
req = webob.Request.blank('/v3/%s/volume_transfers/detail' %
fake.PROJECT_ID)
req.method = 'GET'
req.headers = mv.get_mv_header(mv.TRANSFER_WITH_SNAPSHOTS)
req.headers['Content-Type'] = 'application/json'
req.headers['Accept'] = 'application/json'
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_ctxt))
res_dict = jsonutils.loads(res.body)
self.assertEqual(http_client.OK, res.status_int)
self.assertEqual(6, len(res_dict['transfers'][0]))
self.assertEqual('test_transfer',
res_dict['transfers'][0]['name'])
self.assertEqual(transfer1['id'], res_dict['transfers'][0]['id'])
self.assertEqual(volume_id_1, res_dict['transfers'][0]['volume_id'])
self.assertEqual(False, res_dict['transfers'][0]['no_snapshots'])
self.assertEqual(6, len(res_dict['transfers'][1]))
self.assertEqual('test_transfer',
res_dict['transfers'][1]['name'])
self.assertEqual(transfer2['id'], res_dict['transfers'][1]['id'])
self.assertEqual(volume_id_2, res_dict['transfers'][1]['volume_id'])
self.assertEqual(False, res_dict['transfers'][1]['no_snapshots'])
db.transfer_destroy(context.get_admin_context(), transfer2['id'])
db.transfer_destroy(context.get_admin_context(), transfer1['id'])
db.volume_destroy(context.get_admin_context(), volume_id_2)
db.volume_destroy(context.get_admin_context(), volume_id_1)
def test_create_transfer_json(self):
volume_id = self._create_volume(status='available', size=5)
body = {"transfer": {"name": "transfer1",
"volume_id": volume_id}}
req = webob.Request.blank('/v3/%s/volume_transfers' %
fake.PROJECT_ID)
req.method = 'POST'
req.headers = mv.get_mv_header(mv.TRANSFER_WITH_SNAPSHOTS)
req.headers['Content-Type'] = 'application/json'
req.body = jsonutils.dump_as_bytes(body)
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_ctxt))
res_dict = jsonutils.loads(res.body)
self.assertEqual(http_client.ACCEPTED, res.status_int)
self.assertIn('id', res_dict['transfer'])
self.assertIn('auth_key', res_dict['transfer'])
self.assertIn('created_at', res_dict['transfer'])
self.assertIn('name', res_dict['transfer'])
self.assertIn('volume_id', res_dict['transfer'])
db.volume_destroy(context.get_admin_context(), volume_id)
def test_create_transfer_with_no_snapshots(self):
volume_id = self._create_volume(status='available', size=5)
body = {"transfer": {"name": "transfer1",
"volume_id": volume_id,
'no_snapshots': True}}
req = webob.Request.blank('/v3/%s/volume_transfers' %
fake.PROJECT_ID)
req.method = 'POST'
req.headers = mv.get_mv_header(mv.TRANSFER_WITH_SNAPSHOTS)
req.headers['Content-Type'] = 'application/json'
req.body = jsonutils.dump_as_bytes(body)
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_ctxt))
res_dict = jsonutils.loads(res.body)
self.assertEqual(http_client.ACCEPTED, res.status_int)
self.assertIn('id', res_dict['transfer'])
self.assertIn('auth_key', res_dict['transfer'])
self.assertIn('created_at', res_dict['transfer'])
self.assertIn('name', res_dict['transfer'])
self.assertIn('volume_id', res_dict['transfer'])
self.assertIn('no_snapshots', res_dict['transfer'])
db.volume_destroy(context.get_admin_context(), volume_id)
def test_delete_transfer_awaiting_transfer(self):
volume_id = self._create_volume()
transfer = self._create_transfer(volume_id)
req = webob.Request.blank('/v3/%s/volume_transfers/%s' % (
fake.PROJECT_ID, transfer['id']))
req.method = 'DELETE'
req.headers = mv.get_mv_header(mv.TRANSFER_WITH_SNAPSHOTS)
req.headers['Content-Type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_ctxt))
self.assertEqual(http_client.ACCEPTED, res.status_int)
# verify transfer has been deleted
req = webob.Request.blank('/v3/%s/volume_transfers/%s' % (
fake.PROJECT_ID, transfer['id']))
req.method = 'GET'
req.headers['Content-Type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_ctxt))
res_dict = jsonutils.loads(res.body)
self.assertEqual(http_client.NOT_FOUND, res.status_int)
self.assertEqual(http_client.NOT_FOUND,
res_dict['itemNotFound']['code'])
self.assertEqual('Transfer %s could not be found.' % transfer['id'],
res_dict['itemNotFound']['message'])
self.assertEqual(db.volume_get(context.get_admin_context(),
volume_id)['status'], 'available')
db.volume_destroy(context.get_admin_context(), volume_id)
def test_accept_transfer_volume_id_specified_json(self):
volume_id = self._create_volume()
transfer = self._create_transfer(volume_id)
svc = self.start_service('volume', host='fake_host')
body = {"accept": {"auth_key": transfer['auth_key']}}
req = webob.Request.blank('/v3/%s/volume_transfers/%s/accept' % (
fake.PROJECT_ID, transfer['id']))
req.method = 'POST'
req.headers = mv.get_mv_header(mv.TRANSFER_WITH_SNAPSHOTS)
req.headers['Content-Type'] = 'application/json'
req.body = jsonutils.dump_as_bytes(body)
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_ctxt))
res_dict = jsonutils.loads(res.body)
self.assertEqual(http_client.ACCEPTED, res.status_int)
self.assertEqual(transfer['id'], res_dict['transfer']['id'])
self.assertEqual(volume_id, res_dict['transfer']['volume_id'])
# cleanup
svc.stop()

View File

@ -405,6 +405,10 @@ class MigrationsMixin(test_migrations.WalkVersionsMixin):
volume_attachment = db_utils.get_table(engine, 'volume_attachment') volume_attachment = db_utils.get_table(engine, 'volume_attachment')
self.assertIn('connector', volume_attachment.c) self.assertIn('connector', volume_attachment.c)
def _check_123(self, engine, data):
volume_transfer = db_utils.get_table(engine, 'transfers')
self.assertIn('no_snapshots', volume_transfer.c)
def test_walk_versions(self): def test_walk_versions(self):
self.walk_versions(False, False) self.walk_versions(False, False)
self.assert_each_foreign_key_is_part_of_an_index() self.assert_each_foreign_key_is_part_of_an_index()

View File

@ -118,3 +118,39 @@ class TransfersTableTestCase(test.TestCase):
db.transfer_destroy(nctxt.elevated(), xfer_id2) db.transfer_destroy(nctxt.elevated(), xfer_id2)
xfer = db.transfer_get_all(context.get_admin_context()) xfer = db.transfer_get_all(context.get_admin_context())
self.assertEqual(0, len(xfer), "Unexpected number of transfer records") self.assertEqual(0, len(xfer), "Unexpected number of transfer records")
def test_transfer_accept_with_snapshots(self):
volume_id = utils.create_volume(self.ctxt)['id']
snapshot_id1 = utils.create_snapshot(self.ctxt, volume_id,
status='available')['id']
snapshot_id2 = utils.create_snapshot(self.ctxt, volume_id,
status='available')['id']
xfer_id = self._create_transfer(volume_id)
nctxt = context.RequestContext(user_id=fake.USER2_ID,
project_id=fake.PROJECT2_ID)
db.transfer_accept(nctxt.elevated(), xfer_id, fake.USER2_ID,
fake.PROJECT2_ID)
self.assertEqual(fake.PROJECT2_ID,
db.snapshot_get(nctxt, snapshot_id1)['project_id'])
self.assertEqual(fake.PROJECT2_ID,
db.snapshot_get(nctxt, snapshot_id2)['project_id'])
def test_transfer_accept_with_snapshots_invalid_status(self):
volume_id = utils.create_volume(self.ctxt)['id']
snapshot_id1 = utils.create_snapshot(self.ctxt, volume_id,
status='available')['id']
snapshot_id2 = utils.create_snapshot(self.ctxt, volume_id)['id']
xfer_id = self._create_transfer(volume_id)
nctxt = context.RequestContext(user_id=fake.USER2_ID,
project_id=fake.PROJECT2_ID)
self.assertRaises(exception.InvalidSnapshot, db.transfer_accept,
nctxt.elevated(), xfer_id, fake.USER2_ID,
fake.PROJECT2_ID)
self.assertEqual(fake.PROJECT_ID,
db.snapshot_get(self.ctxt,
snapshot_id1)['project_id'])
self.assertEqual(fake.PROJECT_ID,
db.snapshot_get(self.ctxt,
snapshot_id2)['project_id'])
self.assertEqual('awaiting-transfer',
db.volume_get(self.ctxt, volume_id)['status'])

View File

@ -306,3 +306,58 @@ class VolumeTransferTestCase(test.TestCase):
tx_api.get, tx_api.get,
self.ctxt, self.ctxt,
transfer['id']) transfer['id'])
@mock.patch('cinder.volume.utils.notify_about_volume_usage')
def test_transfer_accept_with_snapshots(self, mock_notify):
svc = self.start_service('volume', host='test_host')
self.addCleanup(svc.stop)
tx_api = transfer_api.API()
volume = utils.create_volume(self.ctxt,
volume_type_id=fake.VOLUME_TYPE_ID,
updated_at=self.updated_at)
utils.create_volume_type(self.ctxt.elevated(),
id=fake.VOLUME_TYPE_ID, name="test_type")
utils.create_snapshot(self.ctxt, volume.id, status='available')
transfer = tx_api.create(self.ctxt, volume.id, 'Description')
# Get volume and snapshot quota before accept
self.ctxt.user_id = fake.USER2_ID
self.ctxt.project_id = fake.PROJECT2_ID
usages = db.quota_usage_get_all_by_project(self.ctxt,
self.ctxt.project_id)
self.assertEqual(0, usages.get('volumes', {}).get('in_use', 0))
self.assertEqual(0, usages.get('snapshots', {}).get('in_use', 0))
tx_api.accept(self.ctxt, transfer['id'], transfer['auth_key'])
volume = objects.Volume.get_by_id(self.ctxt, volume.id)
self.assertEqual(fake.PROJECT2_ID, volume.project_id)
self.assertEqual(fake.USER2_ID, volume.user_id)
calls = [mock.call(self.ctxt, mock.ANY, "transfer.accept.start"),
mock.call(self.ctxt, mock.ANY, "transfer.accept.end")]
mock_notify.assert_has_calls(calls)
# The notify_about_volume_usage is called twice at create(),
# and twice at accept().
self.assertEqual(4, mock_notify.call_count)
# Get volume and snapshot quota after accept
self.ctxt.user_id = fake.USER2_ID
self.ctxt.project_id = fake.PROJECT2_ID
usages = db.quota_usage_get_all_by_project(self.ctxt,
self.ctxt.project_id)
self.assertEqual(1, usages.get('volumes', {}).get('in_use', 0))
self.assertEqual(1, usages.get('snapshots', {}).get('in_use', 0))
@mock.patch('cinder.volume.utils.notify_about_volume_usage')
def test_transfer_accept_with_snapshots_invalid(self, mock_notify):
svc = self.start_service('volume', host='test_host')
self.addCleanup(svc.stop)
tx_api = transfer_api.API()
volume = utils.create_volume(self.ctxt,
volume_type_id=fake.VOLUME_TYPE_ID,
updated_at=self.updated_at)
utils.create_volume_type(self.ctxt.elevated(),
id=fake.VOLUME_TYPE_ID, name="test_type")
utils.create_snapshot(self.ctxt, volume.id, status='deleting')
self.assertRaises(exception.InvalidSnapshot,
tx_api.create, self.ctxt, volume.id, 'Description')

View File

@ -274,8 +274,10 @@ class VolumeRPCAPITestCase(test.RPCAPITestCase):
volume=self.fake_volume_obj, volume=self.fake_volume_obj,
new_user=fake.USER_ID, new_user=fake.USER_ID,
new_project=fake.PROJECT_ID, new_project=fake.PROJECT_ID,
no_snapshots=True,
expected_kwargs_diff={ expected_kwargs_diff={
'volume_id': self.fake_volume_obj.id}) 'volume_id': self.fake_volume_obj.id},
version='3.16')
@ddt.data(None, 'mycluster') @ddt.data(None, 'mycluster')
def test_extend_volume(self, cluster_name): def test_extend_volume(self, cluster_name):

View File

@ -113,7 +113,7 @@ class API(base.Base):
auth_key = auth_key.encode('utf-8') auth_key = auth_key.encode('utf-8')
return hmac.new(salt, auth_key, hashlib.sha1).hexdigest() return hmac.new(salt, auth_key, hashlib.sha1).hexdigest()
def create(self, context, volume_id, display_name): def create(self, context, volume_id, display_name, no_snapshots=False):
"""Creates an entry in the transfers table.""" """Creates an entry in the transfers table."""
LOG.info("Generating transfer record for volume %s", volume_id) LOG.info("Generating transfer record for volume %s", volume_id)
volume_ref = self.db.volume_get(context, volume_id) volume_ref = self.db.volume_get(context, volume_id)
@ -124,6 +124,18 @@ class API(base.Base):
raise exception.InvalidVolume( raise exception.InvalidVolume(
reason=_("transferring encrypted volume is not supported")) reason=_("transferring encrypted volume is not supported"))
if not no_snapshots:
snapshots = self.db.snapshot_get_all_for_volume(context, volume_id)
for snapshot in snapshots:
if snapshot['status'] != "available":
msg = _("snapshot: %s status must be "
"available") % snapshot['id']
raise exception.InvalidSnapshot(reason=msg)
if snapshot.get('encryption_key_id'):
msg = _("snapshot: %s encrypted snapshots cannot be "
"transferred") % snapshot['id']
raise exception.InvalidSnapshot(reason=msg)
volume_utils.notify_about_volume_usage(context, volume_ref, volume_utils.notify_about_volume_usage(context, volume_ref,
"transfer.create.start") "transfer.create.start")
# The salt is just a short random string. # The salt is just a short random string.
@ -136,7 +148,8 @@ class API(base.Base):
'display_name': display_name, 'display_name': display_name,
'salt': salt, 'salt': salt,
'crypt_hash': crypt_hash, 'crypt_hash': crypt_hash,
'expires_at': None} 'expires_at': None,
'no_snapshots': no_snapshots}
try: try:
transfer = self.db.transfer_create(context, transfer_rec) transfer = self.db.transfer_create(context, transfer_rec)
@ -149,7 +162,44 @@ class API(base.Base):
'volume_id': transfer['volume_id'], 'volume_id': transfer['volume_id'],
'display_name': transfer['display_name'], 'display_name': transfer['display_name'],
'auth_key': auth_key, 'auth_key': auth_key,
'created_at': transfer['created_at']} 'created_at': transfer['created_at'],
'no_snapshots': transfer['no_snapshots']}
def _handle_snapshot_quota(self, context, snapshots, volume_type_id,
donor_id):
snapshots_num = len(snapshots)
volume_sizes = 0
if not CONF.no_snapshot_gb_quota:
for snapshot in snapshots:
volume_sizes += snapshot.volume_size
try:
reserve_opts = {'snapshots': snapshots_num,
'gigabytes': volume_sizes}
QUOTAS.add_volume_type_opts(context,
reserve_opts,
volume_type_id)
reservations = QUOTAS.reserve(context, **reserve_opts)
except exception.OverQuota as e:
quota_utils.process_reserve_over_quota(
context, e,
resource='snapshots',
size=volume_sizes)
try:
reserve_opts = {'snapshots': -snapshots_num,
'gigabytes': -volume_sizes}
QUOTAS.add_volume_type_opts(context.elevated(),
reserve_opts,
volume_type_id)
donor_reservations = QUOTAS.reserve(context,
project_id=donor_id,
**reserve_opts)
except exception.OverQuota as e:
donor_reservations = None
LOG.exception("Failed to update volume providing snapshots quota:"
" Over quota.")
return reservations, donor_reservations
def accept(self, context, transfer_id, auth_key): def accept(self, context, transfer_id, auth_key):
"""Accept a volume that has been offered for transfer.""" """Accept a volume that has been offered for transfer."""
@ -206,6 +256,15 @@ class API(base.Base):
LOG.exception("Failed to update quota donating volume" LOG.exception("Failed to update quota donating volume"
" transfer id %s", transfer_id) " transfer id %s", transfer_id)
snap_res = None
snap_donor_res = None
if transfer['no_snapshots'] is False:
snapshots = objects.SnapshotList.get_all_for_volume(
context.elevated(), volume_id)
volume_type_id = vol_ref.volume_type_id
snap_res, snap_donor_res = self._handle_snapshot_quota(
context, snapshots, volume_type_id, vol_ref['project_id'])
volume_utils.notify_about_volume_usage(context, vol_ref, volume_utils.notify_about_volume_usage(context, vol_ref,
"transfer.accept.start") "transfer.accept.start")
try: try:
@ -214,21 +273,32 @@ class API(base.Base):
self.volume_api.accept_transfer(context, self.volume_api.accept_transfer(context,
vol_ref, vol_ref,
context.user_id, context.user_id,
context.project_id) context.project_id,
transfer['no_snapshots'])
self.db.transfer_accept(context.elevated(), self.db.transfer_accept(context.elevated(),
transfer_id, transfer_id,
context.user_id, context.user_id,
context.project_id) context.project_id,
transfer['no_snapshots'])
QUOTAS.commit(context, reservations) QUOTAS.commit(context, reservations)
if snap_res:
QUOTAS.commit(context, snap_res)
if donor_reservations: if donor_reservations:
QUOTAS.commit(context, donor_reservations, project_id=donor_id) QUOTAS.commit(context, donor_reservations, project_id=donor_id)
if snap_donor_res:
QUOTAS.commit(context, snap_donor_res, project_id=donor_id)
LOG.info("Volume %s has been transferred.", volume_id) LOG.info("Volume %s has been transferred.", volume_id)
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
QUOTAS.rollback(context, reservations) QUOTAS.rollback(context, reservations)
if snap_res:
QUOTAS.rollback(context, snap_res)
if donor_reservations: if donor_reservations:
QUOTAS.rollback(context, donor_reservations, QUOTAS.rollback(context, donor_reservations,
project_id=donor_id) project_id=donor_id)
if snap_donor_res:
QUOTAS.rollback(context, snap_donor_res,
project_id=donor_id)
vol_ref = self.db.volume_get(context, volume_id) vol_ref = self.db.volume_get(context, volume_id)
volume_utils.notify_about_volume_usage(context, vol_ref, volume_utils.notify_about_volume_usage(context, vol_ref,

View File

@ -815,7 +815,8 @@ class API(base.Base):
resource=volume) resource=volume)
self.unreserve_volume(context, volume) self.unreserve_volume(context, volume)
def accept_transfer(self, context, volume, new_user, new_project): def accept_transfer(self, context, volume, new_user, new_project,
no_snapshots=False):
context.authorize(vol_transfer_policy.ACCEPT_POLICY, context.authorize(vol_transfer_policy.ACCEPT_POLICY,
target_obj=volume) target_obj=volume)
if volume['status'] == 'maintenance': if volume['status'] == 'maintenance':
@ -826,7 +827,8 @@ class API(base.Base):
results = self.volume_rpcapi.accept_transfer(context, results = self.volume_rpcapi.accept_transfer(context,
volume, volume,
new_user, new_user,
new_project) new_project,
no_snapshots=no_snapshots)
LOG.info("Transfer volume completed successfully.", LOG.info("Transfer volume completed successfully.",
resource=volume) resource=volume)
return results return results

View File

@ -1863,7 +1863,8 @@ class VolumeManager(manager.CleanableManager,
LOG.info("Remove snapshot export completed successfully.", LOG.info("Remove snapshot export completed successfully.",
resource=snapshot) resource=snapshot)
def accept_transfer(self, context, volume_id, new_user, new_project): def accept_transfer(self, context, volume_id, new_user, new_project,
no_snapshots=False):
# NOTE(flaper87): Verify the driver is enabled # NOTE(flaper87): Verify the driver is enabled
# before going forward. The exception will be caught # before going forward. The exception will be caught
# and the volume status updated. # and the volume status updated.

View File

@ -133,9 +133,10 @@ class VolumeAPI(rpc.RPCAPI):
3.14 - Adds enable_replication, disable_replication, 3.14 - Adds enable_replication, disable_replication,
failover_replication, and list_replication_targets. failover_replication, and list_replication_targets.
3.15 - Add revert_to_snapshot method 3.15 - Add revert_to_snapshot method
3.16 - Add no_snapshots to accept_transfer method
""" """
RPC_API_VERSION = '3.15' RPC_API_VERSION = '3.16'
RPC_DEFAULT_VERSION = '3.0' RPC_DEFAULT_VERSION = '3.0'
TOPIC = constants.VOLUME_TOPIC TOPIC = constants.VOLUME_TOPIC
BINARY = constants.VOLUME_BINARY BINARY = constants.VOLUME_BINARY
@ -238,10 +239,17 @@ class VolumeAPI(rpc.RPCAPI):
cctxt = self._get_cctxt(fanout=True) cctxt = self._get_cctxt(fanout=True)
cctxt.cast(ctxt, 'publish_service_capabilities') cctxt.cast(ctxt, 'publish_service_capabilities')
def accept_transfer(self, ctxt, volume, new_user, new_project): def accept_transfer(self, ctxt, volume, new_user, new_project,
cctxt = self._get_cctxt(volume.service_topic_queue) no_snapshots=False):
return cctxt.call(ctxt, 'accept_transfer', volume_id=volume['id'], msg_args = {'volume_id': volume['id'],
new_user=new_user, new_project=new_project) 'new_user': new_user,
'new_project': new_project,
'no_snapshots': no_snapshots
}
cctxt = self._get_cctxt(volume.service_topic_queue, ('3.16', '3.0'))
if not self.client.can_send_version('3.16'):
msg_args.pop('no_snapshots')
return cctxt.call(ctxt, 'accept_transfer', **msg_args)
def extend_volume(self, ctxt, volume, new_size, reservations): def extend_volume(self, ctxt, volume, new_size, reservations):
cctxt = self._get_cctxt(volume.service_topic_queue) cctxt = self._get_cctxt(volume.service_topic_queue)

View File

@ -449,6 +449,14 @@ donor, or original owner, creates a transfer request and sends the created
transfer ID and authorization key to the volume recipient. The volume transfer ID and authorization key to the volume recipient. The volume
recipient, or new owner, accepts the transfer by using the ID and key. recipient, or new owner, accepts the transfer by using the ID and key.
In Rocky, Cinder changes the API behavior for V2 and 3.x < 3.55, snapshots will
be transferred with volume by default. That means if the volume has some
snapshots, when a user transfers a volume from one owner to another, then those
snapshots will be transferred with the volume as well. After microversion 3.55,
Cinder supports the ability to transfer volume without snapshots. If users
don't want to transfer snapshots, they need to specify the new optional
argument `--no_snapshots`.
.. note:: .. note::
The procedure for volume transfer is intended for projects (both the The procedure for volume transfer is intended for projects (both the
@ -484,10 +492,15 @@ Create a volume transfer request
.. code-block:: console .. code-block:: console
$ openstack volume transfer request create <volume> $ openstack volume transfer request create [--no-snapshots] <volume>
<volume> The arguments to be passed are:
Name or ID of volume to transfer.
``<volume>``
Name or ID of volume to transfer.
``--no-snapshots``
Transfer the volume without snapshots.
The volume must be in an ``available`` state or the request will be The volume must be in an ``available`` state or the request will be
denied. If the transfer request is valid in the database (that is, it denied. If the transfer request is valid in the database (that is, it

View File

@ -0,0 +1,6 @@
---
features:
- Support transfer volume with snapshots by default in new V3 API
'v3/volume_transfers'. After microverison 3.55, if users don't want to
transfer snapshots, they could use the new optional argument
`no_snapshots=True` in request body of new transfer creation API.