diff --git a/novaclient/__init__.py b/novaclient/__init__.py index c8e960980..cead09fa6 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -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") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 7161f9da3..7232fb589 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -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'] diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 1aaf76d4f..3fe4e8d97 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -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') diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index a68adeed5..348091d06 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -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') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index c9346e504..e1dec382a 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -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)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index cd328f9c0..a036bee5c 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -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, diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 4df02f88a..20a28b9ab 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -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( diff --git a/releasenotes/notes/microversion-v2_45-1bfcae3914280534.yaml b/releasenotes/notes/microversion-v2_45-1bfcae3914280534.yaml new file mode 100644 index 000000000..1c278f3e1 --- /dev/null +++ b/releasenotes/notes/microversion-v2_45-1bfcae3914280534.yaml @@ -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.