diff --git a/glanceclient/tests/unit/v2/base.py b/glanceclient/tests/unit/v2/base.py index 043acb12..c595f41c 100644 --- a/glanceclient/tests/unit/v2/base.py +++ b/glanceclient/tests/unit/v2/base.py @@ -117,6 +117,15 @@ class BaseController(testtools.TestCase): resp = self.controller.image_import(*args, **kwargs) self._assertRequestId(resp) + def add_image_location(self, *args): + resp = self.controller.add_image_location(*args) + self._assertRequestId(resp) + + def get_image_locations(self, *args): + resource = self.controller.get_image_locations(*args) + self._assertRequestId(resource) + return resource + class BaseResourceTypeController(BaseController): def __init__(self, api, schema_api, controller_class): diff --git a/glanceclient/tests/unit/v2/test_images.py b/glanceclient/tests/unit/v2/test_images.py index 3ce12c16..8a82774b 100644 --- a/glanceclient/tests/unit/v2/test_images.py +++ b/glanceclient/tests/unit/v2/test_images.py @@ -688,6 +688,18 @@ data_fixtures = { ]}, ), }, + '/v2/images/3a4560a1-e585-443e-9b39-553b46ec92d1/locations': { + 'POST': ({}, '') + }, + '/v2/images/a2b83adc-888e-11e3-8872-78acc0b951d8/locations': { + 'GET': ( + {}, + [{'url': 'http://foo.com/', + 'metadata': {'store': 'cheap'}}, + {'url': 'http://bar.com/', + 'metadata': {'store': 'fast'}}], '' + ), + }, } schema_fixtures = { @@ -1503,3 +1515,60 @@ class TestController(testtools.TestCase): self.controller.update_location, image_id, **new_loc) self.assertIn(err_str, str(err)) + + def test_add_image_location(self): + location_url = 'http://spam.com/' + image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' + expect = [ + ('POST', + '/v2/images/%s/locations' % (image_id), + {}, + [('url', location_url), ('validation_data', {})]), + ('GET', '/v2/images/%s' % (image_id), {}, None)] + with mock.patch.object(common_utils, + 'has_version') as mock_has_version: + mock_has_version.return_value = True + self.controller.add_image_location(image_id, location_url, {}) + self.assertEqual(expect, self.api.calls) + + def test_add_image_location_with_validation_data(self): + location_url = 'http://spam.com/' + image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' + expect = [ + ('POST', + '/v2/images/%s/locations' % (image_id), + {}, + [('url', location_url), ('validation_data', + {'os_hash_algo': 'sha512'})]), + ('GET', '/v2/images/%s' % (image_id), {}, None)] + with mock.patch.object(common_utils, + 'has_version') as mock_has_version: + mock_has_version.return_value = True + self.controller.add_image_location(image_id, location_url, + {'os_hash_algo': 'sha512'}) + self.assertEqual(expect, self.api.calls) + + def test_add_image_location_not_supported(self): + with mock.patch.object(common_utils, + 'has_version') as mock_has_version: + mock_has_version.return_value = False + self.assertRaises(exc.HTTPNotImplemented, + self.controller.add_image_location, + '3a4560a1-e585-443e-9b39-553b46ec92d1', + 'http://spam.com/') + + def test_get_image_locations(self): + image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' + with mock.patch.object(common_utils, + 'has_version') as mock_has_version: + mock_has_version.return_value = True + locations = self.controller.get_image_locations(image_id) + self.assertEqual(2, len(locations)) + + def test_get_image_location_not_supported(self): + with mock.patch.object(common_utils, + 'has_version') as mock_has_version: + mock_has_version.return_value = False + self.assertRaises(exc.HTTPNotImplemented, + self.controller.get_image_locations, + '3a4560a1-e585-443e-9b39-553b46ec92d1') diff --git a/glanceclient/tests/unit/v2/test_shell_v2.py b/glanceclient/tests/unit/v2/test_shell_v2.py index bd37cf64..e5a33b00 100644 --- a/glanceclient/tests/unit/v2/test_shell_v2.py +++ b/glanceclient/tests/unit/v2/test_shell_v2.py @@ -1989,6 +1989,68 @@ class ShellV2Test(testtools.TestCase): loc['metadata']) utils.print_dict.assert_called_once_with(expect_image) + def test_do_add_location(self): + gc = self.gc + url = 'http://foo.com/', + validation_data = {'os_hash_algo': 'sha512', + 'os_hash_value': 'value'} + args = {'id': 'IMG-01', + 'url': url, + 'validation_data': json.dumps(validation_data)} + with mock.patch.object(gc.images, + 'add_image_location') as mock_addloc: + expect_image = {'id': 'pass'} + mock_addloc.return_value = expect_image + + test_shell.do_add_location(self.gc, self._make_args(args)) + mock_addloc.assert_called_once_with( + 'IMG-01', url, validation_data=validation_data) + utils.print_dict.assert_called_once_with(expect_image) + + @mock.patch('glanceclient.common.utils.exit') + def test_do_add_location_with_checksum_in_validation_data(self, + mock_exit): + validation_data = {'checksum': 'value', + 'os_hash_algo': 'sha512', + 'os_hash_value': 'value'} + + args = self._make_args( + {'id': 'IMG-01', 'url': 'http://foo.com/', + 'validation_data': json.dumps(validation_data)}) + expected_msg = ('Validation Data should contain only os_hash_algo' + ' and os_hash_value. `checksum` is not allowed') + mock_exit.side_effect = self._mock_utils_exit + try: + test_shell.do_add_location(self.gc, args) + self.fail("utils.exit should have been called") + except SystemExit: + pass + + mock_exit.assert_called_once_with(expected_msg) + + @mock.patch('glanceclient.common.utils.exit') + def test_do_add_location_with_invalid_algo_in_validation_data(self, + mock_exit): + validation_data = {'os_hash_algo': 'algo', + 'os_hash_value': 'value'} + + args = self._make_args( + {'id': 'IMG-01', 'url': 'http://foo.com/', + 'validation_data': json.dumps(validation_data)}) + allowed_hash_algo = ['sha512', 'sha256', 'sha1', 'md5'] + expected_msg = ('os_hash_algo: `%s` is incorrect, ' + 'allowed hashing algorithms: %s' % + (validation_data['os_hash_algo'], + allowed_hash_algo)) + mock_exit.side_effect = self._mock_utils_exit + try: + test_shell.do_add_location(self.gc, args) + self.fail("utils.exit should have been called") + except SystemExit: + pass + + mock_exit.assert_called_once_with(expected_msg) + def test_image_upload(self): args = self._make_args( {'id': 'IMG-01', 'file': 'test', 'size': 1024, 'progress': False}) diff --git a/glanceclient/v2/images.py b/glanceclient/v2/images.py index 216837d0..94c3d12c 100644 --- a/glanceclient/v2/images.py +++ b/glanceclient/v2/images.py @@ -560,3 +560,33 @@ class Controller(object): req_id_hdr = {'x-openstack-request-id': response.request_ids[0]} return self._get(image_id, req_id_hdr) + + def add_image_location(self, image_id, location_url, validation_data={}): + """Add a new location to an image. + + :param image_id: ID of image to which the location is to be added. + :param location_url: URL of the location to add. + :param validation_data: Validation data for the image. + """ + if not utils.has_version(self.http_client, 'v2.17'): + raise exc.HTTPNotImplemented( + 'This operation is not supported by Glance.') + + url = '/v2/images/%s/locations' % image_id + data = {'url': location_url, + 'validation_data': validation_data} + resp, body = self.http_client.post(url, data=data) + return self._get(image_id) + + @utils.add_req_id_to_object() + def get_image_locations(self, image_id): + """Fetch list of locations associated to the Image. + + :param image_id: ID of image to which the location is to be fetched. + """ + if not utils.has_version(self.http_client, 'v2.17'): + raise exc.HTTPNotImplemented( + 'This operation is not supported by Glance.') + url = '/v2/images/%s/locations' % (image_id) + resp, locations = self.http_client.get(url) + return locations, resp diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py index 974e7904..0b647876 100644 --- a/glanceclient/v2/shell.py +++ b/glanceclient/v2/shell.py @@ -1012,6 +1012,43 @@ def do_location_update(gc, args): utils.print_dict(image) +@utils.arg('--url', metavar='', required=True, + help=_('URL of location to add.')) +@utils.arg('--validation-data', metavar='', default='{}', + help=_('Validation data containing os_hash_algo and os_hash_value ' + 'only associated to the image. Must be a valid JSON object ' + '(default: %(default)s)')) +@utils.arg('id', metavar='', + help=_('ID of image whose location is to be added.')) +def do_add_location(gc, args): + """Add location to an image which is in `queued` state only. """ + try: + invalid_val_data = None + validation_data = json.loads(args.validation_data) + accepted_values = ['os_hash_algo', 'os_hash_value'] + invalid_val_data = list(set(validation_data.keys()).difference( + accepted_values)) + if invalid_val_data: + utils.exit('Validation Data should contain only os_hash_algo ' + 'and os_hash_value. `%s` is not allowed' % + (*invalid_val_data,)) + + allowed_hash_algo = ['sha512', 'sha256', 'sha1', 'md5'] + if validation_data and \ + validation_data['os_hash_algo'] not in allowed_hash_algo: + raise utils.exit('os_hash_algo: `%s` is incorrect, ' + 'allowed hashing algorithms: %s' % + (validation_data['os_hash_algo'], + allowed_hash_algo)) + + except ValueError: + utils.exit('validation-data is not a valid JSON object.') + else: + image = gc.images.add_image_location(args.id, args.url, + validation_data=validation_data) + utils.print_image(image) + + # Metadata - catalog NAMESPACE_SCHEMA = None diff --git a/releasenotes/notes/add_new_locations_apis_support-1ceb47178d384d58.yaml b/releasenotes/notes/add_new_locations_apis_support-1ceb47178d384d58.yaml new file mode 100644 index 00000000..801aa0c9 --- /dev/null +++ b/releasenotes/notes/add_new_locations_apis_support-1ceb47178d384d58.yaml @@ -0,0 +1,23 @@ +--- +features: + - | + Add support for the new Glance ``Locations`` APIs + + - Add client support for newly added API, + ``POST /v2/images/{image_id}/locations`` in Glance. + New add location operation is allowed for service to service + interaction, end users only when `http` store is enabled in + deployment and images which are in ``queued`` state only. + This api replaces the image-update (old location-add) mechanism + for consumers like cinder and nova to address `OSSN-0090`_ and + `OSSN-0065`_. This client change adds support of new shell command + ``add-location`` and new client method ``add_image_location``. + - Add support for newly added API, + ``GET /v2/images/{image_id}/locations`` in Glance to fetch the + locations associated to an image. This change adds new client method + ``get_image_locations`` since this new get locations api is meant for + service user only hence it is not exposed to the end user as a shell + command. + + .. _OSSN-0090: https://wiki.openstack.org/wiki/OSSN/OSSN-0090 + .. _OSSN-0065: https://wiki.openstack.org/wiki/OSSN/OSSN-0065