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
This commit is contained in:
parent
19334f0f8d
commit
ae6e288072
@ -273,6 +273,8 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
|||||||
for key in ['id', 'name', 'created_at', 'updated_at', 'tags', 'size',
|
for key in ['id', 'name', 'created_at', 'updated_at', 'tags', 'size',
|
||||||
'owner', 'checksum', 'status']:
|
'owner', 'checksum', 'status']:
|
||||||
_image[key] = image[key]
|
_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['visibility'] = 'public' if image['is_public'] else 'private'
|
||||||
_image = self.schema.filter(_image)
|
_image = self.schema.filter(_image)
|
||||||
_image['self'] = self._get_image_href(image)
|
_image['self'] = self._get_image_href(image)
|
||||||
@ -380,6 +382,10 @@ _BASE_PROPERTIES = {
|
|||||||
'maxLength': 255,
|
'maxLength': 255,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'direct_url': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'URL to access the image file kept in external store',
|
||||||
|
},
|
||||||
'self': {'type': 'string'},
|
'self': {'type': 'string'},
|
||||||
'access': {'type': 'string'},
|
'access': {'type': 'string'},
|
||||||
'file': {'type': 'string'},
|
'file': {'type': 'string'},
|
||||||
|
@ -47,6 +47,10 @@ common_opts = [
|
|||||||
cfg.IntOpt('api_limit_max', default=1000,
|
cfg.IntOpt('api_limit_max', default=1000,
|
||||||
help=_('Maximum permissible number of items that could be '
|
help=_('Maximum permissible number of items that could be '
|
||||||
'returned by a request')),
|
'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
|
CONF = cfg.CONF
|
||||||
|
@ -66,6 +66,7 @@ class Server(object):
|
|||||||
self.server_control = './bin/glance-control'
|
self.server_control = './bin/glance-control'
|
||||||
self.exec_env = None
|
self.exec_env = None
|
||||||
self.deployment_flavor = ''
|
self.deployment_flavor = ''
|
||||||
|
self.show_image_direct_url = False
|
||||||
self.server_control_options = ''
|
self.server_control_options = ''
|
||||||
self.needs_database = False
|
self.needs_database = False
|
||||||
|
|
||||||
@ -253,6 +254,7 @@ policy_file = %(policy_file)s
|
|||||||
policy_default_rule = %(policy_default_rule)s
|
policy_default_rule = %(policy_default_rule)s
|
||||||
db_auto_create = False
|
db_auto_create = False
|
||||||
sql_connection = %(sql_connection)s
|
sql_connection = %(sql_connection)s
|
||||||
|
show_image_direct_url = %(show_image_direct_url)s
|
||||||
[paste_deploy]
|
[paste_deploy]
|
||||||
flavor = %(deployment_flavor)s
|
flavor = %(deployment_flavor)s
|
||||||
"""
|
"""
|
||||||
|
@ -535,3 +535,123 @@ class TestImages(functional.FunctionalTest):
|
|||||||
self.assertEqual(400, response.status_code)
|
self.assertEqual(400, response.status_code)
|
||||||
|
|
||||||
self.stop_servers()
|
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)
|
||||||
|
@ -52,6 +52,7 @@ class TestSchemas(functional.FunctionalTest):
|
|||||||
'status',
|
'status',
|
||||||
'access',
|
'access',
|
||||||
'schema',
|
'schema',
|
||||||
|
'direct_url',
|
||||||
])
|
])
|
||||||
self.assertEqual(expected, set(image_schema['properties'].keys()))
|
self.assertEqual(expected, set(image_schema['properties'].keys()))
|
||||||
|
|
||||||
|
@ -1284,3 +1284,83 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
|
|||||||
response = webob.Response()
|
response = webob.Response()
|
||||||
serializer.show(response, self.fixture)
|
serializer.show(response, self.fixture)
|
||||||
self.assertEqual(expected, json.loads(response.body))
|
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user