From ae6e288072f833b11c557747d354c6e5ba71a439 Mon Sep 17 00:00:00 2001 From: Nikhil Komawar Date: Sun, 5 Aug 2012 10:24:16 -0500 Subject: [PATCH] Allows exposing image location based on config It would be good to allow exposing image location to trusted clients, say Nova. Thus, eventually helping for internal deployments. This merge prop enables us to set a config value which will let us either expose the image location or not expose it. Edits from markwash: * unit test refactoring * should not return direct_url as None, that would violate the schema which requires direct_url to, when present, be a string implements bp api-v2-store-access Change-Id: I69da3b4ce3fc9261cbf448e797e48affa56281ef --- glance/api/v2/images.py | 6 + glance/common/config.py | 4 + glance/tests/functional/__init__.py | 2 + glance/tests/functional/v2/test_images.py | 120 +++++++++++++++++++ glance/tests/functional/v2/test_schemas.py | 1 + glance/tests/unit/v2/test_images_resource.py | 80 +++++++++++++ 6 files changed, 213 insertions(+) diff --git a/glance/api/v2/images.py b/glance/api/v2/images.py index 31c079cfa2..509bc65b73 100644 --- a/glance/api/v2/images.py +++ b/glance/api/v2/images.py @@ -273,6 +273,8 @@ class ResponseSerializer(wsgi.JSONResponseSerializer): for key in ['id', 'name', 'created_at', 'updated_at', 'tags', 'size', 'owner', 'checksum', 'status']: _image[key] = image[key] + if CONF.show_image_direct_url and image['location']: + _image['direct_url'] = image['location'] _image['visibility'] = 'public' if image['is_public'] else 'private' _image = self.schema.filter(_image) _image['self'] = self._get_image_href(image) @@ -380,6 +382,10 @@ _BASE_PROPERTIES = { 'maxLength': 255, }, }, + 'direct_url': { + 'type': 'string', + 'description': 'URL to access the image file kept in external store', + }, 'self': {'type': 'string'}, 'access': {'type': 'string'}, 'file': {'type': 'string'}, diff --git a/glance/common/config.py b/glance/common/config.py index 53a9fa6fc9..0ce34a7141 100644 --- a/glance/common/config.py +++ b/glance/common/config.py @@ -47,6 +47,10 @@ common_opts = [ cfg.IntOpt('api_limit_max', default=1000, help=_('Maximum permissible number of items that could be ' 'returned by a request')), + cfg.BoolOpt('show_image_direct_url', default=False, + help=_('Whether to include the backend image storage location ' + 'in image properties. Revealing storage location can be a ' + 'security risk, so use this setting with caution!')), ] CONF = cfg.CONF diff --git a/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py index ea8be0a3cf..0fc26be216 100644 --- a/glance/tests/functional/__init__.py +++ b/glance/tests/functional/__init__.py @@ -66,6 +66,7 @@ class Server(object): self.server_control = './bin/glance-control' self.exec_env = None self.deployment_flavor = '' + self.show_image_direct_url = False self.server_control_options = '' self.needs_database = False @@ -253,6 +254,7 @@ policy_file = %(policy_file)s policy_default_rule = %(policy_default_rule)s db_auto_create = False sql_connection = %(sql_connection)s +show_image_direct_url = %(show_image_direct_url)s [paste_deploy] flavor = %(deployment_flavor)s """ diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py index 0552658866..098e0afc17 100644 --- a/glance/tests/functional/v2/test_images.py +++ b/glance/tests/functional/v2/test_images.py @@ -535,3 +535,123 @@ class TestImages(functional.FunctionalTest): self.assertEqual(400, response.status_code) self.stop_servers() + + +class TestImageDirectURLVisibility(functional.FunctionalTest): + + def setUp(self): + super(TestImageDirectURLVisibility, self).setUp() + self.cleanup() + self.api_server.deployment_flavor = 'noauth' + + def _url(self, path): + return 'http://127.0.0.1:%d%s' % (self.api_port, path) + + def _headers(self, custom_headers=None): + base_headers = { + 'X-Identity-Status': 'Confirmed', + 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96', + 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e', + 'X-Tenant-Id': TENANT1, + 'X-Roles': 'member', + } + base_headers.update(custom_headers or {}) + return base_headers + + def test_image_direct_url_visible(self): + + self.api_server.show_image_direct_url = True + self.start_servers(**self.__dict__.copy()) + + # Image list should be empty + path = self._url('/v2/images') + response = requests.get(path, headers=self._headers()) + self.assertEqual(200, response.status_code) + images = json.loads(response.text)['images'] + self.assertEqual(0, len(images)) + + # Create an image + path = self._url('/v2/images') + headers = self._headers({'content-type': 'application/json'}) + data = json.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar'}) + response = requests.post(path, headers=headers, data=data) + self.assertEqual(200, response.status_code) + + # Get the image id + image = json.loads(response.text) + image_id = image['id'] + + # Image direct_url should not be visible before location is set + path = self._url('/v2/images/%s' % image_id) + headers = self._headers({'Content-Type': 'application/json'}) + response = requests.get(path, headers=headers) + self.assertEqual(200, response.status_code) + image = json.loads(response.text) + self.assertFalse('direct_url' in image) + + # Upload some image data, setting the image location + path = self._url('/v2/images/%s/file' % image_id) + headers = self._headers({'Content-Type': 'application/octet-stream'}) + response = requests.put(path, headers=headers, data='ZZZZZ') + self.assertEqual(200, response.status_code) + + # Image direct_url should be visible + path = self._url('/v2/images/%s' % image_id) + headers = self._headers({'Content-Type': 'application/json'}) + response = requests.get(path, headers=headers) + self.assertEqual(200, response.status_code) + image = json.loads(response.text) + self.assertTrue('direct_url' in image) + + # Image direct_url should be visible in a list + path = self._url('/v2/images') + headers = self._headers({'Content-Type': 'application/json'}) + response = requests.get(path, headers=headers) + self.assertEqual(200, response.status_code) + image = json.loads(response.text)['images'][0] + self.assertTrue('direct_url' in image) + + def test_image_direct_url_not_visible(self): + + self.api_server.show_image_direct_url = False + self.start_servers(**self.__dict__.copy()) + + # Image list should be empty + path = self._url('/v2/images') + response = requests.get(path, headers=self._headers()) + self.assertEqual(200, response.status_code) + images = json.loads(response.text)['images'] + self.assertEqual(0, len(images)) + + # Create an image + path = self._url('/v2/images') + headers = self._headers({'content-type': 'application/json'}) + data = json.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar'}) + response = requests.post(path, headers=headers, data=data) + self.assertEqual(200, response.status_code) + + # Get the image id + image = json.loads(response.text) + image_id = image['id'] + + # Upload some image data, setting the image location + path = self._url('/v2/images/%s/file' % image_id) + headers = self._headers({'Content-Type': 'application/octet-stream'}) + response = requests.put(path, headers=headers, data='ZZZZZ') + self.assertEqual(200, response.status_code) + + # Image direct_url should not be visible + path = self._url('/v2/images/%s' % image_id) + headers = self._headers({'Content-Type': 'application/json'}) + response = requests.get(path, headers=headers) + self.assertEqual(200, response.status_code) + image = json.loads(response.text) + self.assertFalse('direct_url' in image) + + # Image direct_url should not be visible in a list + path = self._url('/v2/images') + headers = self._headers({'Content-Type': 'application/json'}) + response = requests.get(path, headers=headers) + self.assertEqual(200, response.status_code) + image = json.loads(response.text)['images'][0] + self.assertFalse('direct_url' in image) diff --git a/glance/tests/functional/v2/test_schemas.py b/glance/tests/functional/v2/test_schemas.py index 488967cafc..826cd5720d 100644 --- a/glance/tests/functional/v2/test_schemas.py +++ b/glance/tests/functional/v2/test_schemas.py @@ -52,6 +52,7 @@ class TestSchemas(functional.FunctionalTest): 'status', 'access', 'schema', + 'direct_url', ]) self.assertEqual(expected, set(image_schema['properties'].keys())) diff --git a/glance/tests/unit/v2/test_images_resource.py b/glance/tests/unit/v2/test_images_resource.py index 9260f516ea..45d3ec64f3 100644 --- a/glance/tests/unit/v2/test_images_resource.py +++ b/glance/tests/unit/v2/test_images_resource.py @@ -1284,3 +1284,83 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase): response = webob.Response() serializer.show(response, self.fixture) self.assertEqual(expected, json.loads(response.body)) + + +class TestImagesSerializerDirectUrl(test_utils.BaseTestCase): + + def setUp(self): + super(TestImagesSerializerDirectUrl, self).setUp() + self.serializer = glance.api.v2.images.ResponseSerializer() + self.active_image = { + 'id': unit_test_utils.UUID1, + 'name': 'image-1', + 'is_public': True, + 'properties': {}, + 'checksum': None, + 'owner': TENANT1, + 'status': 'active', + 'created_at': DATETIME, + 'updated_at': DATETIME, + 'tags': ['one', 'two'], + 'size': 1024, + 'location': 'http://some/fake/location', + } + self.queued_image = { + 'id': unit_test_utils.UUID2, + 'name': 'image-2', + 'is_public': False, + 'owner': None, + 'status': 'queued', + 'properties': {}, + 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', + 'created_at': DATETIME, + 'updated_at': DATETIME, + 'tags': [], + 'size': None, + 'location': None, + } + self.fixtures = [self.active_image, self.queued_image] + + def _do_index(self): + request = webob.Request.blank('/v2/images') + response = webob.Response(request=request) + self.serializer.index(response, {'images': self.fixtures}) + return json.loads(response.body)['images'] + + def _do_show(self, image): + request = webob.Request.blank('/v2/images') + response = webob.Response(request=request) + self.serializer.show(response, image) + return json.loads(response.body) + + def test_index_store_location_enabled(self): + self.config(show_image_direct_url=True) + images = self._do_index() + + # NOTE(markwash): ordering sanity check + self.assertEqual(images[0]['id'], unit_test_utils.UUID1) + self.assertEqual(images[1]['id'], unit_test_utils.UUID2) + + self.assertEqual(images[0]['direct_url'], 'http://some/fake/location') + self.assertFalse('direct_url' in images[1]) + + def test_index_store_location_explicitly_disabled(self): + self.config(show_image_direct_url=False) + images = self._do_index() + self.assertFalse('direct_url' in images[0]) + self.assertFalse('direct_url' in images[1]) + + def test_show_location_enabled(self): + self.config(show_image_direct_url=True) + image = self._do_show(self.active_image) + self.assertEqual(image['direct_url'], 'http://some/fake/location') + + def test_show_location_enabled_but_not_set(self): + self.config(show_image_direct_url=True) + image = self._do_show(self.queued_image) + self.assertFalse('direct_url' in image) + + def test_show_location_explicitly_disabled(self): + self.config(show_image_direct_url=False) + image = self._do_show(self.active_image) + self.assertFalse('direct_url' in image)