Improve volume transfer records

Add a new microversion API to add source_project_id,
destination_project_id, accepted fields to the response of follow
APIs:

- Create a volume transfer
- Show volume transfer detail
- List volume transfer and detail

And the source_project_id will be recorded when a transfer is
created, the destination_project_id and accepted will be recorded
when a transfer is accepted.

Part of blueprint: improve-volume-transfer-records

Change-Id: I3d79f9a67a9aed7272871969e7c0942a1f396ea5
This commit is contained in:
Yikun Jiang 2018-11-06 11:35:22 +08:00
parent 39e78e44e0
commit 3e16dfff8e
17 changed files with 180 additions and 22 deletions

View File

@ -410,6 +410,13 @@ absolute:
in: body
required: true
type: object
accepted:
description: |
Records if this transfer was accepted or not.
in: body
required: false
type: boolean
min_version: 3.57
active_backend_id:
description: |
The ID of active storage backend. Only in ``cinder-volume`` service.
@ -967,6 +974,13 @@ description_volume_type_required:
in: body
required: true
type: string
destination_project_id:
description: |
Records the destination project_id after volume transfer.
in: body
required: false
type: string
min_version: 3.57
detached_at:
description: |
The time when attachment is detached.
@ -2571,6 +2585,13 @@ source_group_id_req:
in: body
required: true
type: string
source_project_id:
description: |
Records the source project_id before volume transfer.
in: body
required: false
type: string
min_version: 3.57
source_reference:
description: |
The snapshot's origin volume information.

View File

@ -22,7 +22,7 @@
"min_version": "3.0",
"status": "CURRENT",
"updated": "2018-07-17T00:00:00Z",
"version": "3.56"
"version": "3.57"
}
]
}

View File

@ -46,7 +46,7 @@
"min_version": "3.0",
"status": "CURRENT",
"updated": "2018-07-17T00:00:00Z",
"version": "3.56"
"version": "3.57"
}
]
}

View File

@ -5,6 +5,9 @@
"name": "first volume",
"volume_id": "c86b9af4-151d-4ead-b62c-5fb967af0e37",
"auth_key": "9266c59563c84664",
"source_project_id": "10f92548903841278443fcf3da935b22",
"destination_project_id": null,
"accepted": false,
"links": [
{
"href": "http://localhost/v3/firstproject/volumes/3",

View File

@ -4,6 +4,9 @@
"created_at": "2015-02-25T03:56:53.081642",
"name": "first volume transfer",
"volume_id": "894623a6-e901-4312-aa06-4275e6321cce",
"source_project_id": "10f92548903841278443fcf3da935b22",
"destination_project_id": null,
"accepted": false,
"links": [
{
"href": "http://localhost/v3/firstproject/volumes/1",

View File

@ -5,6 +5,9 @@
"created_at": "2015-02-25T03:56:53.081642",
"name": "first volume transfer",
"volume_id": "894623a6-e901-4312-aa06-4275e6321cce",
"source_project_id": "10f92548903841278443fcf3da935b22",
"destination_project_id": null,
"accepted": false,
"links": [
{
"href": "http://localhost/v3/firstproject/volumes/1",
@ -21,6 +24,9 @@
"created_at": "2015-03-25T03:56:53.081642",
"name": "second volume transfer",
"volume_id": "673db275-379f-41af-8371-e1652132b4c1",
"source_project_id": "10f92548903841278443fcf3da935b22",
"destination_project_id": null,
"accepted": false,
"links": [
{
"href": "http://localhost/v3/firstproject/volumes/2",

View File

@ -114,6 +114,9 @@ Response Parameters
- volume_id: volume_id
- id: id
- name: name
- destination_project_id: destination_project_id
- source_project_id: source_project_id
- accepted: accepted
Response Example
----------------
@ -198,6 +201,9 @@ Response Parameters
- id: id
- links: links
- name: name
- destination_project_id: destination_project_id
- source_project_id: source_project_id
- accepted: accepted
Response Example
@ -264,6 +270,9 @@ Response Parameters
- id: id
- links: links
- name: name
- destination_project_id: destination_project_id
- source_project_id: source_project_id
- accepted: accepted
Response Example
----------------

View File

@ -153,6 +153,8 @@ TRANSFER_WITH_SNAPSHOTS = '3.55'
BACKUP_PROJECT_USER_ID = '3.56'
TRANSFER_WITH_HISTORY = '3.57'
def get_mv_header(version):
"""Gets a formatted HTTP microversion header.

View File

@ -129,6 +129,8 @@ REST_API_VERSION_HISTORY = """
* 3.55 - Support transfer volume with snapshots
* 3.56 - Add ``user_id`` attribute to response body of list backup with
detail and show backup detail APIs.
* 3.57 - Add 'source_project_id', 'destination_project_id', 'accepted' to
transfer.
"""
# The minimum and maximum versions of the API supported
@ -136,7 +138,7 @@ REST_API_VERSION_HISTORY = """
# minimum version of the API supported.
# Explicitly using /v2 endpoints will still work
_MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.56"
_MAX_API_VERSION = "3.57"
_LEGACY_API_VERSION2 = "2.0"
UPDATED = "2018-07-17T00:00:00Z"

View File

@ -446,3 +446,9 @@ Support ability to transfer snapshots along with their parent volume.
----
Add ``user_id`` attribute to response body of list backup with detail and show
backup detail APIs.
3.57
----
Expanded volume transfer record details by adding ``source_project_id``,
``destination_project_id`` and ``accepted`` fields to ``transfer`` table and
related api (create/show/list detail transfer APIs) responses.

View File

@ -63,6 +63,13 @@ class ViewBuilder(common.ViewBuilder):
if req_version.matches(mv.TRANSFER_WITH_SNAPSHOTS):
detail_body['transfer'].update({'no_snapshots':
transfer.get('no_snapshots')})
if req_version.matches(mv.TRANSFER_WITH_HISTORY):
transfer_history = {
'destination_project_id': transfer['destination_project_id'],
'source_project_id': transfer['source_project_id'],
'accepted': transfer['accepted']
}
detail_body['transfer'].update(transfer_history)
return detail_body
def create(self, request, transfer):
@ -81,6 +88,13 @@ class ViewBuilder(common.ViewBuilder):
if req_version.matches(mv.TRANSFER_WITH_SNAPSHOTS):
create_body['transfer'].update({'no_snapshots':
transfer.get('no_snapshots')})
if req_version.matches(mv.TRANSFER_WITH_HISTORY):
transfer_history = {
'destination_project_id': transfer['destination_project_id'],
'source_project_id': transfer['source_project_id'],
'accepted': transfer['accepted']
}
create_body['transfer'].update(transfer_history)
return create_body
def _list_view(self, func, request, transfers, origin_transfer_count):

View File

@ -5426,7 +5426,8 @@ def transfer_get(context, transfer_id):
def _translate_transfers(transfers):
fields = ('id', 'volume_id', 'display_name', 'created_at', 'deleted',
'no_snapshots')
'no_snapshots', 'source_project_id', 'destination_project_id',
'accepted')
return [{k: transfer[k] for k in fields} for transfer in transfers]
@ -5578,7 +5579,9 @@ def transfer_accept(context, transfer_id, user_id, project_id,
.filter_by(id=transfer_id)
.update({'deleted': True,
'deleted_at': timeutils.utcnow(),
'updated_at': literal_column('updated_at')}))
'updated_at': literal_column('updated_at'),
'destination_project_id': project_id,
'accepted': True}))
###############################

View File

@ -35,6 +35,11 @@ import cinder.transfer
class VolumeTransferAPITestCase(test.TestCase):
"""Test Case for transfers V3 API."""
microversion = mv.TRANSFER_WITH_SNAPSHOTS
expect_transfer_history = False
DETAIL_LEN = 6
SUMMARY_LEN = 4
def setUp(self):
super(VolumeTransferAPITestCase, self).setUp()
self.volume_transfer_api = cinder.transfer.API()
@ -73,13 +78,24 @@ class VolumeTransferAPITestCase(test.TestCase):
volume_id)
return volume_id
def _check_history_in_res(self, transfer_dict):
tx_history_keys = ['source_project_id',
'destination_project_id',
'accepted']
if self.expect_transfer_history:
for key in tx_history_keys:
self.assertIn(key, transfer_dict)
else:
for key in tx_history_keys:
self.assertNotIn(key, transfer_dict)
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 = mv.get_mv_header(self.microversion)
req.headers['Content-Type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_ctxt))
@ -98,17 +114,17 @@ class VolumeTransferAPITestCase(test.TestCase):
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 = mv.get_mv_header(self.microversion)
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(self.SUMMARY_LEN, 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(self.SUMMARY_LEN, len(res_dict['transfers'][1]))
self.assertEqual(transfer2['id'], res_dict['transfers'][1]['id'])
self.assertEqual('test_transfer', res_dict['transfers'][1]['name'])
@ -121,7 +137,7 @@ class VolumeTransferAPITestCase(test.TestCase):
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 = mv.get_mv_header(self.microversion)
req.headers['Content-Type'] = 'application/json'
req.headers['Accept'] = 'application/json'
res = req.get_response(fakes.wsgi_app(
@ -129,17 +145,19 @@ class VolumeTransferAPITestCase(test.TestCase):
res_dict = jsonutils.loads(res.body)
self.assertEqual(http_client.OK, res.status_int)
self.assertEqual(6, len(res_dict['transfers'][0]))
self.assertEqual(self.DETAIL_LEN, 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._check_history_in_res(res_dict['transfers'][0])
self.assertEqual(6, len(res_dict['transfers'][1]))
self.assertEqual(self.DETAIL_LEN, 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._check_history_in_res(res_dict['transfers'][1])
def test_list_transfers_detail_with_no_snapshots(self):
volume_id_1 = self._create_volume(size=5)
@ -150,7 +168,7 @@ class VolumeTransferAPITestCase(test.TestCase):
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 = mv.get_mv_header(self.microversion)
req.headers['Content-Type'] = 'application/json'
req.headers['Accept'] = 'application/json'
res = req.get_response(fakes.wsgi_app(
@ -158,14 +176,14 @@ class VolumeTransferAPITestCase(test.TestCase):
res_dict = jsonutils.loads(res.body)
self.assertEqual(http_client.OK, res.status_int)
self.assertEqual(6, len(res_dict['transfers'][0]))
self.assertEqual(self.DETAIL_LEN, 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(self.DETAIL_LEN, len(res_dict['transfers'][1]))
self.assertEqual('test_transfer',
res_dict['transfers'][1]['name'])
self.assertEqual(transfer2['id'], res_dict['transfers'][1]['id'])
@ -180,7 +198,7 @@ class VolumeTransferAPITestCase(test.TestCase):
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 = mv.get_mv_header(self.microversion)
req.headers['Content-Type'] = 'application/json'
req.body = jsonutils.dump_as_bytes(body)
res = req.get_response(fakes.wsgi_app(
@ -194,6 +212,7 @@ class VolumeTransferAPITestCase(test.TestCase):
self.assertIn('created_at', res_dict['transfer'])
self.assertIn('name', res_dict['transfer'])
self.assertIn('volume_id', res_dict['transfer'])
self._check_history_in_res(res_dict['transfer'])
def test_create_transfer_with_no_snapshots(self):
volume_id = self._create_volume(status='available', size=5)
@ -204,7 +223,7 @@ class VolumeTransferAPITestCase(test.TestCase):
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 = mv.get_mv_header(self.microversion)
req.headers['Content-Type'] = 'application/json'
req.body = jsonutils.dump_as_bytes(body)
res = req.get_response(fakes.wsgi_app(
@ -219,6 +238,7 @@ class VolumeTransferAPITestCase(test.TestCase):
self.assertIn('name', res_dict['transfer'])
self.assertIn('volume_id', res_dict['transfer'])
self.assertIn('no_snapshots', res_dict['transfer'])
self._check_history_in_res(res_dict['transfer'])
def test_delete_transfer_awaiting_transfer(self):
volume_id = self._create_volume()
@ -227,7 +247,7 @@ class VolumeTransferAPITestCase(test.TestCase):
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 = mv.get_mv_header(self.microversion)
req.headers['Content-Type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_ctxt))
@ -261,7 +281,7 @@ class VolumeTransferAPITestCase(test.TestCase):
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 = mv.get_mv_header(self.microversion)
req.headers['Content-Type'] = 'application/json'
req.body = jsonutils.dump_as_bytes(body)
res = req.get_response(fakes.wsgi_app(
@ -273,3 +293,10 @@ class VolumeTransferAPITestCase(test.TestCase):
self.assertEqual(volume_id, res_dict['transfer']['volume_id'])
# cleanup
svc.stop()
class VolumeTransferAPITestCase357(VolumeTransferAPITestCase):
microversion = mv.TRANSFER_WITH_HISTORY
DETAIL_LEN = 9
expect_transfer_history = True

View File

@ -17,6 +17,8 @@
from cinder import context
from cinder import db
from cinder.db.sqlalchemy import api as db_api
from cinder.db.sqlalchemy import models
from cinder import exception
from cinder import test
from cinder.tests.unit import fake_constants as fake
@ -31,13 +33,15 @@ class TransfersTableTestCase(test.TestCase):
self.ctxt = context.RequestContext(user_id=fake.USER_ID,
project_id=fake.PROJECT_ID)
def _create_transfer(self, volume_id=None):
def _create_transfer(self, volume_id=None, source_project_id=None):
"""Create a transfer object."""
transfer = {'display_name': 'display_name',
'salt': 'salt',
'crypt_hash': 'crypt_hash'}
if volume_id is not None:
transfer['volume_id'] = volume_id
if source_project_id is not None:
transfer['source_project_id'] = source_project_id
return db.transfer_create(self.ctxt, transfer)['id']
def test_transfer_create(self):
@ -119,6 +123,26 @@ class TransfersTableTestCase(test.TestCase):
xfer = db.transfer_get_all(context.get_admin_context())
self.assertEqual(0, len(xfer), "Unexpected number of transfer records")
def test_transfer_accept(self):
volume = utils.create_volume(self.ctxt)
xfer_id = self._create_transfer(volume['id'], volume['project_id'])
nctxt = context.RequestContext(user_id=fake.USER2_ID,
project_id=fake.PROJECT2_ID)
xfer = db.transfer_get(nctxt.elevated(), xfer_id)
self.assertEqual(volume.project_id, xfer['source_project_id'])
self.assertFalse(xfer['accepted'])
self.assertIsNone(xfer['destination_project_id'])
db.transfer_accept(nctxt.elevated(), xfer_id, fake.USER2_ID,
fake.PROJECT2_ID)
xfer = db_api.model_query(
nctxt.elevated(), models.Transfer, read_deleted='yes'
).filter_by(id=xfer_id).first()
self.assertEqual(volume.project_id, xfer['source_project_id'])
self.assertTrue(xfer['accepted'])
self.assertEqual(fake.PROJECT2_ID, xfer['destination_project_id'])
def test_transfer_accept_with_snapshots(self):
volume_id = utils.create_volume(self.ctxt)['id']
snapshot_id1 = utils.create_snapshot(self.ctxt, volume_id,

View File

@ -18,6 +18,8 @@ from oslo_utils import timeutils
from cinder import context
from cinder import db
from cinder.db.sqlalchemy import api as db_api
from cinder.db.sqlalchemy import models
from cinder import exception
from cinder import objects
from cinder import quota
@ -361,3 +363,28 @@ class VolumeTransferTestCase(test.TestCase):
utils.create_snapshot(self.ctxt, volume.id, status='deleting')
self.assertRaises(exception.InvalidSnapshot,
tx_api.create, self.ctxt, volume.id, 'Description')
@mock.patch('cinder.volume.utils.notify_about_volume_usage')
def test_transfer_accept_with_detail_records(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, updated_at=self.updated_at)
transfer = tx_api.create(self.ctxt, volume.id, 'Description')
self.assertEqual(volume.project_id, transfer['source_project_id'])
self.assertIsNone(transfer['destination_project_id'])
self.assertFalse(transfer['accepted'])
# Get volume and snapshot quota before accept
self.ctxt.user_id = fake.USER2_ID
self.ctxt.project_id = fake.PROJECT2_ID
tx_api.accept(self.ctxt, transfer['id'], transfer['auth_key'])
xfer = db_api.model_query(self.ctxt, models.Transfer,
read_deleted='yes'
).filter_by(id=transfer['id']).first()
self.assertEqual(volume.project_id, xfer['source_project_id'])
self.assertTrue(xfer['accepted'])
self.assertEqual(fake.PROJECT2_ID, xfer['destination_project_id'])

View File

@ -150,7 +150,8 @@ class API(base.Base):
'salt': salt,
'crypt_hash': crypt_hash,
'expires_at': None,
'no_snapshots': no_snapshots}
'no_snapshots': no_snapshots,
'source_project_id': volume_ref['project_id']}
try:
transfer = self.db.transfer_create(context, transfer_rec)
@ -164,7 +165,10 @@ class API(base.Base):
'display_name': transfer['display_name'],
'auth_key': auth_key,
'created_at': transfer['created_at'],
'no_snapshots': transfer['no_snapshots']}
'no_snapshots': transfer['no_snapshots'],
'source_project_id': transfer['source_project_id'],
'destination_project_id': transfer['destination_project_id'],
'accepted': transfer['accepted']}
def _handle_snapshot_quota(self, context, snapshots, volume_type_id,
donor_id):

View File

@ -0,0 +1,7 @@
---
features:
- |
Expanded volume transfer information. Starting with microversion 3.57,
``source_project_id``, ``destination_project_id``, and ``accepted`` fields
will be returned in the response of the volume transfer create, show, and
list calls.