2.45: Remove Location header from createImage and createBackup responses

This changes the response for the createImage and createBackup
server action APIs to no longer return a Location header and instead
returns a json dict body with the snapshot image ID. This is done
in a new microversion.

Implements blueprint remove-create-image-location-header-response

Closes-Bug: #1679285

Change-Id: Idc899ee76b8265b1c9e0871b6c7c277424cdd442
This commit is contained in:
Matt Riedemann 2017-04-20 16:46:24 -04:00
parent ec1794b18c
commit 66b0cf3337
21 changed files with 193 additions and 13 deletions

View File

@ -3,9 +3,14 @@ image_location:
description: | description: |
The image location URL of the image or backup created, HTTP header The image location URL of the image or backup created, HTTP header
"Location: <image location URL>" will be returned. "Location: <image location URL>" will be returned.
.. note:: The URL returned may not be accessible to users and should not
be relied upon. Use microversion 2.45 or simply parse the image ID out
of the URL in the Location response header.
in: header in: header
required: true required: true
type: string type: string
max_version: 2.44
tag_location: tag_location:
description: | description: |
The location of the tag. It's individual tag URL which can be used for The location of the tag. It's individual tag URL which can be used for
@ -4582,6 +4587,13 @@ snapshot_id_optional:
in: body in: body
required: false required: false
type: string type: string
snapshot_id_resp_2_45:
description: |
The UUID for the resulting image snapshot.
in: body
required: true
type: string
min_version: 2.45
snapshot_name: snapshot_name:
description: | description: |
The snapshot name. The snapshot name.

View File

@ -244,7 +244,7 @@ Request
- rotation: backup_rotation - rotation: backup_rotation
- metadata: metadata - metadata: metadata
**Example Create Server Back Up (Createbackup Action)** **Example Create Server Back Up (createBackup Action)**
.. literalinclude:: ../../doc/api_samples/os-create-backup/create-backup-req.json .. literalinclude:: ../../doc/api_samples/os-create-backup/create-backup-req.json
:language: javascript :language: javascript
@ -254,7 +254,13 @@ Response
.. rest_parameters:: parameters.yaml .. rest_parameters:: parameters.yaml
- image_location: image_location - Location: image_location
- image_id: snapshot_id_resp_2_45
**Example Create Server Back Up (v2.45)**
.. literalinclude:: ../../doc/api_samples/os-create-backup/v2.45/create-backup-resp.json
:language: javascript
Create Image (createImage Action) Create Image (createImage Action)
@ -329,6 +335,12 @@ Response
.. rest_parameters:: parameters.yaml .. rest_parameters:: parameters.yaml
- Location: image_location - Location: image_location
- image_id: snapshot_id_resp_2_45
**Example Create Image (v2.45)**
.. literalinclude:: ../../doc/api_samples/servers/v2.45/server-action-create-image-resp.json
:language: javascript
Lock Server (lock Action) Lock Server (lock Action)

View File

@ -0,0 +1,7 @@
{
"createBackup": {
"name": "Backup 1",
"backup_type": "weekly",
"rotation": 1
}
}

View File

@ -0,0 +1,3 @@
{
"image_id": "0e7761dd-ee98-41f0-ba35-05994e446431"
}

View File

@ -0,0 +1,3 @@
{
"image_id": "0e7761dd-ee98-41f0-ba35-05994e446431"
}

View File

@ -0,0 +1,8 @@
{
"createImage" : {
"name" : "foo-image",
"metadata": {
"meta_var": "meta_val"
}
}
}

View File

@ -19,7 +19,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.44", "version": "2.45",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@ -22,7 +22,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.44", "version": "2.45",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@ -105,6 +105,10 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 2.43 - Deprecate os-hosts API * 2.43 - Deprecate os-hosts API
* 2.44 - The servers action addFixedIp, removeFixedIp, addFloatingIp, * 2.44 - The servers action addFixedIp, removeFixedIp, addFloatingIp,
removeFloatingIp and os-virtual-interfaces APIs are deprecated. removeFloatingIp and os-virtual-interfaces APIs are deprecated.
* 2.45 - The createImage and createBackup APIs no longer return a Location
header in the response for the snapshot image, they now return a
json dict in the response body with an image_id key and uuid
value.
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
@ -113,7 +117,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions # Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API. # support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1" _MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.44" _MAX_API_VERSION = "2.45"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which related to network, images and baremetal # Almost all proxy APIs which related to network, images and baremetal

View File

@ -33,6 +33,7 @@ class CreateBackupController(wsgi.Controller):
super(CreateBackupController, self).__init__(*args, **kwargs) super(CreateBackupController, self).__init__(*args, **kwargs)
self.compute_api = compute.API() self.compute_api = compute.API()
@wsgi.response(202)
@extensions.expected_errors((400, 403, 404, 409)) @extensions.expected_errors((400, 403, 404, 409))
@wsgi.action('createBackup') @wsgi.action('createBackup')
@validation.schema(create_backup.create_backup_v20, '2.0', '2.0') @validation.schema(create_backup.create_backup_v20, '2.0', '2.0')
@ -78,6 +79,11 @@ class CreateBackupController(wsgi.Controller):
except exception.InvalidRequest as e: except exception.InvalidRequest as e:
raise webob.exc.HTTPBadRequest(explanation=e.format_message()) raise webob.exc.HTTPBadRequest(explanation=e.format_message())
# Starting with microversion 2.45 we return a response body containing
# the snapshot image id without the Location header.
if api_version_request.is_supported(req, '2.45'):
return {'image_id': image['id']}
resp = webob.Response(status_int=202) resp = webob.Response(status_int=202)
# build location of newly-created image entity if rotation is not zero # build location of newly-created image entity if rotation is not zero

View File

@ -529,3 +529,10 @@ user documentation.
To query attached neutron interfaces for a specific server, the API To query attached neutron interfaces for a specific server, the API
`GET /servers/{server_uuid}/os-interface` can be used. `GET /servers/{server_uuid}/os-interface` can be used.
2.45
----
The ``createImage`` and ``createBackup`` server action APIs no longer return
a ``Location`` header in the response for the snapshot image, they now return
a json dict in the response body with an ``image_id`` key and uuid value.

View File

@ -1017,6 +1017,11 @@ class ServersController(wsgi.Controller):
except exception.Invalid as err: except exception.Invalid as err:
raise exc.HTTPBadRequest(explanation=err.format_message()) raise exc.HTTPBadRequest(explanation=err.format_message())
# Starting with microversion 2.45 we return a response body containing
# the snapshot image id without the Location header.
if api_version_request.is_supported(req, '2.45'):
return {'image_id': image['id']}
# build location of newly-created image entity # build location of newly-created image entity
image_id = str(image['id']) image_id = str(image['id'])
image_ref = glance.generate_image_url(image_id) image_ref = glance.generate_image_url(image_id)

View File

@ -0,0 +1,7 @@
{
"createBackup": {
"name": "Backup 1",
"backup_type": "weekly",
"rotation": 1
}
}

View File

@ -0,0 +1,3 @@
{
"image_id": "%(uuid)s"
}

View File

@ -0,0 +1,9 @@
{
"createImage" : {
"name" : "%(name)s",
"metadata": {
"meta_var": "meta_val"
}
}
}

View File

@ -36,3 +36,21 @@ class CreateBackupSamplesJsonTest(test_servers.ServersSampleBase):
response = self._do_post('servers/%s/action' % self.uuid, response = self._do_post('servers/%s/action' % self.uuid,
'create-backup-req', {}) 'create-backup-req', {})
self.assertEqual(202, response.status_code) self.assertEqual(202, response.status_code)
# we should have gotten a location header back
self.assertIn('location', response.headers)
# we should not have gotten a body back
self.assertEqual(0, len(response.content))
class CreateBackupSamplesJsonTestv2_45(CreateBackupSamplesJsonTest):
"""Tests the createBackup server action API with microversion 2.45."""
microversion = '2.45'
scenarios = [('v2_45', {'api_major_version': 'v2.1'})]
def test_post_backup_server(self):
# Get api samples to backup server request.
response = self._do_post('servers/%s/action' % self.uuid,
'create-backup-req', {})
self._verify_response('create-backup-resp', {}, response, 202)
# assert that no location header was returned
self.assertNotIn('location', response.headers)

View File

@ -225,8 +225,7 @@ class ServerSortKeysJsonTests(ServersSampleBase):
200) 200)
class ServersActionsJsonTest(ServersSampleBase): class _ServersActionsJsonTestMixin(object):
def _test_server_action(self, uuid, action, req_tpl, def _test_server_action(self, uuid, action, req_tpl,
subs=None, resp_tpl=None, code=202): subs=None, resp_tpl=None, code=202):
subs = subs or {} subs = subs or {}
@ -240,6 +239,10 @@ class ServersActionsJsonTest(ServersSampleBase):
else: else:
self.assertEqual(code, response.status_code) self.assertEqual(code, response.status_code)
self.assertEqual("", response.text) self.assertEqual("", response.text)
return response
class ServersActionsJsonTest(ServersSampleBase, _ServersActionsJsonTestMixin):
def test_server_reboot_hard(self): def test_server_reboot_hard(self):
uuid = self._post_server() uuid = self._post_server()
@ -291,12 +294,6 @@ class ServersActionsJsonTest(ServersSampleBase):
'server-action-confirm-resize', 'server-action-confirm-resize',
code=204) code=204)
def test_server_create_image(self):
uuid = self._post_server()
self._test_server_action(uuid, 'createImage',
'server-action-create-image',
{'name': 'foo-image'})
def _wait_for_active_server(self, uuid): def _wait_for_active_server(self, uuid):
"""Wait 10 seconds for the server to be ACTIVE, else fail. """Wait 10 seconds for the server to be ACTIVE, else fail.
@ -370,6 +367,34 @@ class ServersActionsJson219Test(ServersSampleBase):
self._verify_response('server-action-rebuild-resp', subs, resp, 202) self._verify_response('server-action-rebuild-resp', subs, resp, 202)
class ServersCreateImageJsonTest(ServersSampleBase,
_ServersActionsJsonTestMixin):
"""Tests the createImage server action API against 2.1."""
def test_server_create_image(self):
uuid = self._post_server()
resp = self._test_server_action(uuid, 'createImage',
'server-action-create-image',
{'name': 'foo-image'})
# we should have gotten a location header back
self.assertIn('location', resp.headers)
# we should not have gotten a body back
self.assertEqual(0, len(resp.content))
class ServersCreateImageJsonTestv2_45(ServersCreateImageJsonTest):
"""Tests the createImage server action API against 2.45."""
microversion = '2.45'
scenarios = [('v2_45', {'api_major_version': 'v2.1'})]
def test_server_create_image(self):
uuid = self._post_server()
resp = self._test_server_action(
uuid, 'createImage', 'server-action-create-image',
{'name': 'foo-image'}, 'server-action-create-image-resp')
# assert that no location header was returned
self.assertNotIn('location', resp.headers)
class ServerStartStopJsonTest(ServersSampleBase): class ServerStartStopJsonTest(ServersSampleBase):
def _test_server_action(self, uuid, action, req_tpl): def _test_server_action(self, uuid, action, req_tpl):

View File

@ -283,6 +283,27 @@ class CreateBackupTestsV21(admin_only_action_common.CommonMixin,
self.assertEqual(202, res.status_int) self.assertEqual(202, res.status_int)
self.assertIn('fake-image-id', res.headers['Location']) self.assertIn('fake-image-id', res.headers['Location'])
@mock.patch.object(common, 'check_img_metadata_properties_quota')
@mock.patch.object(api.API, 'backup', return_value=dict(
id='fake-image-id', status='ACTIVE', name='Backup 1', properties={}))
def test_create_backup_v2_45(self, mock_backup, mock_check_image):
"""Tests the 2.45 microversion to ensure the Location header is not
in the response.
"""
body = {
'createBackup': {
'name': 'Backup 1',
'backup_type': 'daily',
'rotation': '1',
},
}
instance = fake_instance.fake_instance_obj(self.context)
self.mock_get.return_value = instance
req = fakes.HTTPRequest.blank('', version='2.45')
res = self.controller._create_backup(req, instance['uuid'], body=body)
self.assertIsInstance(res, dict)
self.assertEqual('fake-image-id', res['image_id'])
@mock.patch.object(common, 'check_img_metadata_properties_quota') @mock.patch.object(common, 'check_img_metadata_properties_quota')
@mock.patch.object(api.API, 'backup') @mock.patch.object(api.API, 'backup')
def test_create_backup_raises_conflict_on_invalid_state(self, def test_create_backup_raises_conflict_on_invalid_state(self,

View File

@ -901,6 +901,21 @@ class ServerActionsControllerTestV21(test.TestCase):
glance.generate_image_url('123'), glance.generate_image_url('123'),
location) location)
def test_create_image_v2_45(self):
"""Tests the createImage server action API with the 2.45 microversion
where there is a response body but no Location header.
"""
body = {
'createImage': {
'name': 'Snapshot 1',
},
}
req = fakes.HTTPRequest.blank('', version='2.45')
response = self.controller._action_create_image(req, FAKE_UUID,
body=body)
self.assertIsInstance(response, dict)
self.assertEqual('123', response['image_id'])
def test_create_image_name_too_long(self): def test_create_image_name_too_long(self):
long_name = 'a' * 260 long_name = 'a' * 260
body = { body = {

View File

@ -0,0 +1,11 @@
---
other:
- |
The 2.45 microversion is introduced which changes the response for the
``createImage`` and ``createBackup`` server action APIs to no longer
return a ``Location`` response header. With microversion 2.45 those APIs
now return a json dict in the response body with a single ``image_id`` key
whose value is the snapshot image ID (a uuid). The old ``Location`` header
in the response before microversion 2.45 is most likely broken and
inaccessible by end users since it relies on the internal Glance API
server configuration and does not take into account Glance API versions.