2.45: createImage/createBackup image_id is in response body

This adds support for microversion 2.45 which changes the response
on the createImage and createBackup server action APIs to return
the image_id for the created snapshot image in a json dict in the
response body rather than in a Location header (which is gone in
microversion >= 2.45).

Since the 'nova backup' command was not printing out the created
image ID before, that is also added in this microversion.

Part of blueprint remove-create-image-location-header-response

Change-Id: Id48aa7b14e2d25008287549b04db437ca9c3f548
This commit is contained in:
Matt Riedemann 2017-04-14 21:07:45 -04:00
parent e303cf11bf
commit 603f0eae9f
8 changed files with 150 additions and 14 deletions

View File

@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1")
# when client supported the max version, and bumped sequentially, otherwise
# the client may break due to server side new version may include some
# backward incompatible change.
API_MAX_VERSION = api_versions.APIVersion("2.44")
API_MAX_VERSION = api_versions.APIVersion("2.45")

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import api_versions
from novaclient.tests.unit import fakes
from novaclient.tests.unit.fixture_data import base
from novaclient.tests.unit.v2 import fakes as v2_fakes
@ -443,6 +444,8 @@ class V1(Base):
context.status_code = 202
assert len(body.keys()) == 1
action = list(body)[0]
api_version = api_versions.APIVersion(
request.headers.get('X-OpenStack-Nova-API-Version', '2.1'))
if v2_fakes.FakeSessionClient.check_server_actions(body):
# NOTE(snikitin): No need to do any operations here. This 'pass'
@ -475,6 +478,14 @@ class V1(Base):
_body = {'adminPass': 'RescuePassword'}
elif action == 'createImage':
assert set(body[action].keys()) == set(['name', 'metadata'])
if api_version >= api_versions.APIVersion('2.45'):
return {'image_id': '456'}
context.headers['location'] = "http://blah/images/456"
elif action == 'createBackup':
assert set(body[action].keys()) == set(['name', 'backup_type',
'rotation'])
if api_version >= api_versions.APIVersion('2.45'):
return {'image_id': '456'}
context.headers['location'] = "http://blah/images/456"
elif action == 'os-getConsoleOutput':
assert list(body[action]) == ['length']

View File

@ -47,6 +47,7 @@ FAKE_IMAGE_UUID_1 = 'c99d7632-bd66-4be9-aed5-3dd14b223a76'
FAKE_IMAGE_UUID_2 = 'f27f479a-ddda-419a-9bbc-d6b56b210161'
FAKE_IMAGE_UUID_SNAPSHOT = '555cae93-fb41-4145-9c52-f5b923538a26'
FAKE_IMAGE_UUID_SNAP_DEL = '55bb23af-97a4-4068-bdf8-f10c62880ddf'
FAKE_IMAGE_UUID_BACKUP = '2f87e889-41a4-4778-8553-83f5eea68c5d'
# fake request id
FAKE_REQUEST_ID = fakes.FAKE_REQUEST_ID
@ -716,9 +717,6 @@ class FakeSessionClient(base_client.SessionClient):
assert body[action] is None
elif action in ['addSecurityGroup', 'removeSecurityGroup']:
assert list(body[action]) == ['name']
elif action == 'createBackup':
assert set(body[action]) == set(['name', 'backup_type',
'rotation'])
elif action == 'trigger_crash_dump':
assert body[action] is None
else:
@ -766,11 +764,22 @@ class FakeSessionClient(base_client.SessionClient):
_body = {'adminPass': 'RescuePassword'}
elif action == 'createImage':
assert set(body[action].keys()) == set(['name', 'metadata'])
_headers = dict(location="http://blah/images/%s" %
FAKE_IMAGE_UUID_SNAPSHOT)
if self.api_version < api_versions.APIVersion('2.45'):
_headers = dict(location="http://blah/images/%s" %
FAKE_IMAGE_UUID_SNAPSHOT)
else:
_body = {'image_id': FAKE_IMAGE_UUID_SNAPSHOT}
if body[action]['name'] == 'mysnapshot_deleted':
_headers = dict(location="http://blah/images/%s" %
FAKE_IMAGE_UUID_SNAP_DEL)
elif action == 'createBackup':
assert set(body[action].keys()) == set(['name', 'backup_type',
'rotation'])
if self.api_version < api_versions.APIVersion('2.45'):
_headers = dict(location="http://blah/images/%s" %
FAKE_IMAGE_UUID_BACKUP)
else:
_body = {'image_id': FAKE_IMAGE_UUID_BACKUP}
elif action == 'os-getConsoleOutput':
assert list(body[action]) == ['length']
return (202, {}, {'output': 'foo'})
@ -1039,7 +1048,17 @@ class FakeSessionClient(base_client.SessionClient):
"status": "SAVING",
"progress": 80,
"links": {},
}
},
{
"id": FAKE_IMAGE_UUID_BACKUP,
"name": "back1",
"serverId": '1234',
"updated": "2010-10-10T12:00:00Z",
"created": "2010-08-10T12:00:00Z",
"status": "SAVING",
"progress": 80,
"links": {},
},
]})
def get_images_555cae93_fb41_4145_9c52_f5b923538a26(self, **kw):
@ -1054,6 +1073,9 @@ class FakeSessionClient(base_client.SessionClient):
def get_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw):
return (200, {}, {'image': self.get_images()[2]['images'][3]})
def get_images_2f87e889_41a4_4778_8553_83f5eea68c5d(self, **kw):
return (200, {}, {'image': self.get_images()[2]['images'][4]})
def get_images_3e861307_73a6_4d1f_8d68_f68b03223032(self):
raise exceptions.NotFound('404')

View File

@ -1409,3 +1409,51 @@ class ServersV2_37Test(ServersV226Test):
def test_remove_floating_ip(self):
# self.floating_ips.list() is not available after 2.35
pass
class ServersCreateImageBackupV2_45Test(utils.FixturedTestCase):
"""Tests the 2.45 microversion for createImage and createBackup
server actions.
"""
client_fixture_class = client.V1
data_fixture_class = data.V1
api_version = '2.45'
def setUp(self):
super(ServersCreateImageBackupV2_45Test, self).setUp()
self.cs.api_version = api_versions.APIVersion(self.api_version)
def test_create_image(self):
"""Tests the createImage API with the 2.45 microversion which
does not return the Location header, it returns a json dict in the
response body with an image_id key.
"""
s = self.cs.servers.get(1234)
im = s.create_image('123')
self.assertEqual('456', im)
self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers/1234/action')
im = s.create_image('123', {})
self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers/1234/action')
im = self.cs.servers.create_image(s, '123')
self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers/1234/action')
im = self.cs.servers.create_image(s, '123', {})
self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers/1234/action')
def test_backup(self):
s = self.cs.servers.get(1234)
# Test backup on the Server object.
sb = s.backup('back1', 'daily', 1)
self.assertIn('image_id', sb)
self.assertEqual('456', sb['image_id'])
self.assert_request_id(sb, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers/1234/action')
# Test backup on the ServerManager.
sb = self.cs.servers.backup(s, 'back1', 'daily', 2)
self.assertEqual('456', sb['image_id'])
self.assert_request_id(sb, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers/1234/action')

View File

@ -1139,6 +1139,18 @@ class ShellTest(utils.TestCase):
{'createImage': {'name': 'mysnapshot', 'metadata': {}}},
)
def test_create_image_2_45(self):
"""Tests the image-create command with microversion 2.45 which
does not change the output of the command, just how the response
from the server is processed.
"""
self.run_command('image-create sample-server mysnapshot',
api_version='2.45')
self.assert_called(
'POST', '/servers/1234/action',
{'createImage': {'name': 'mysnapshot', 'metadata': {}}},
)
def test_create_image_with_incorrect_metadata(self):
cmd = 'image-create sample-server mysnapshot --metadata foo'
result = self.assertRaises(argparse.ArgumentTypeError,
@ -2518,7 +2530,9 @@ class ShellTest(utils.TestCase):
{'removeFixedIp': {'address': '10.0.0.10'}})
def test_backup(self):
self.run_command('backup sample-server back1 daily 1')
out, err = self.run_command('backup sample-server back1 daily 1')
# With microversion < 2.45 there is no output from this command.
self.assertEqual(0, len(out))
self.assert_called('POST', '/servers/1234/action',
{'createBackup': {'name': 'back1',
'backup_type': 'daily',
@ -2529,6 +2543,23 @@ class ShellTest(utils.TestCase):
'backup_type': 'daily',
'rotation': '1'}})
def test_backup_2_45(self):
"""Tests the backup command with the 2.45 microversion which
handles a different response and prints out the backup snapshot
image details.
"""
out, err = self.run_command(
'backup sample-server back1 daily 1',
api_version='2.45')
# We should see the backup snapshot image name in the output.
self.assertIn('back1', out)
self.assertIn('SAVING', out)
self.assert_called_anytime(
'POST', '/servers/1234/action',
{'createBackup': {'name': 'back1',
'backup_type': 'daily',
'rotation': '1'}})
def test_limits(self):
self.run_command('limits')
self.assert_called('GET', '/limits')
@ -2914,6 +2945,7 @@ class ShellTest(utils.TestCase):
42, # There are no version-wrapped shell method changes for this.
43, # There are no version-wrapped shell method changes for this.
44, # There are no version-wrapped shell method changes for this.
45, # There are no version-wrapped shell method changes for this.
])
versions_supported = set(range(0,
novaclient.API_MAX_VERSION.ver_minor + 1))

View File

@ -1630,8 +1630,13 @@ class ServerManager(base.BootingManagerWithFind):
body = {'name': image_name, 'metadata': metadata or {}}
resp, body = self._action_return_resp_and_body('createImage', server,
body)
location = resp.headers['location']
image_uuid = location.split('/')[-1]
# The 2.45 microversion returns the image_id in the response body,
# not as a location header.
if self.api_version >= api_versions.APIVersion('2.45'):
image_uuid = body['image_id']
else:
location = resp.headers['location']
image_uuid = location.split('/')[-1]
return base.StrWithMeta(image_uuid, resp)
def backup(self, server, backup_name, backup_type, rotation):
@ -1643,7 +1648,8 @@ class ServerManager(base.BootingManagerWithFind):
:param backup_type: The backup type, like 'daily' or 'weekly'
:param rotation: Int parameter representing how many backups to
keep around.
:returns: An instance of novaclient.base.TupleWithMeta
:returns: An instance of novaclient.base.TupleWithMeta if the request
microversion is < 2.45, otherwise novaclient.base.DictWithMeta.
"""
body = {'name': backup_name,
'backup_type': backup_type,

View File

@ -2040,9 +2040,13 @@ def do_image_create(cs, args):
'around.'))
def do_backup(cs, args):
"""Backup a server by creating a 'backup' type snapshot."""
_find_server(cs, args.server).backup(args.name,
args.backup_type,
args.rotation)
result = _find_server(cs, args.server).backup(args.name,
args.backup_type,
args.rotation)
# Microversion >= 2.45 will return a DictWithMeta that has the image_id
# in it for the backup snapshot image.
if cs.api_version >= api_versions.APIVersion('2.45'):
_print_image(_find_image(cs, result['image_id']))
@utils.arg(

View File

@ -0,0 +1,13 @@
---
features:
- |
Support was added for microversion 2.45. This changes how the
``createImage`` and ``createBackup`` server action APIs return
the created snapshot image ID in the response. With microversion
2.45 and later, the image ID is return in a json dict response body
with an ``image_id`` key and uuid value. The old ``Location`` response
header is no longer returned in microversion 2.45 or later.
There are no changes to the ``nova image-create`` CLI. However, the
``nova backup`` CLI will print out the backup snapshot image information
with microversion 2.45 or greater now.