diff -Nru glance-2012.2~f2~20120524.1545/ChangeLog glance-2012.2~f2~20120524.1548/ChangeLog --- glance-2012.2~f2~20120524.1545/ChangeLog 2012-05-24 22:27:28.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/ChangeLog 2012-05-24 22:37:42.000000000 +0000 @@ -1,3 +1,14 @@ +commit 9a838a8b34e435223e1e01d73d84665e6e8f7102 +Merge: 26ce3e0 d0e6b83 +Author: Jenkins +Date: Thu May 24 22:35:46 2012 +0000 + + Merge changes I26378e9f,I8b09677f + + * changes: + Add allow_additional_image_properties + Fix integration of image properties in v2 API + commit 26ce3e0e4cd94d57f3f4c2aaaf87b35c0643cc21 Merge: 4f0e72c 3785ec8 Author: Jenkins @@ -5,6 +16,28 @@ Merge "Leave behind sqlite DB for red functional tests." +commit d0e6b8398a1d3787ec9ba77ddd1036c47549dafa +Author: Mark Washenberger +Date: Tue May 22 14:13:09 2012 -0400 + + Add allow_additional_image_properties + + Implements bp:api-v2-user-properties + + Change-Id: I26378e9f4e8d0f53898f4665bad89922c0b5792a + + glance/api/v2/images.py | 2 + + glance/api/v2/router.py | 2 +- + glance/common/config.py | 6 + + glance/schema.py | 9 +- + glance/tests/functional/test_schema.py | 11 +- + glance/tests/functional/v2/test_images.py | 12 +- + glance/tests/unit/test_schema.py | 80 ++++++++++- + glance/tests/unit/v2/test_image_access_resource.py | 7 +- + glance/tests/unit/v2/test_images_resource.py | 157 +++++++++++++++++++- + glance/tests/unit/v2/test_schemas_resource.py | 6 +- + 10 files changed, 273 insertions(+), 19 deletions(-) + commit 4f0e72c52ebc4b2d77c5635d543d06fa62f366c9 Merge: 9d3fd05 d2f04a2 Author: Jenkins @@ -12,6 +45,28 @@ Merge "Remove unused imports in setup.py" +commit 397884be58d21589002756e1a712ef5b816bafe2 +Author: Brian Waldon +Date: Mon May 21 21:20:55 2012 -0700 + + Fix integration of image properties in v2 API + + The (de)serialization of images in the v2 API expected a db-like + properties list in some cases, while in others a flat dictionary. This + patch aligns the code by ensuring the properties dictionaries are + normalized before being passed in and out of the controller. + + * Expands the images functional test case to cover this integration point + * Cast all images to dictionaries before returning from the controller + * Related to bp api-2 + + Change-Id: I8b09677f622151330630fc7b0267f0549bb1a458 + + glance/api/v2/images.py | 45 ++++++++++++++++++----- + glance/tests/functional/v2/test_images.py | 7 ++-- + glance/tests/unit/v2/test_images_resource.py | 51 ++++++++++++++++---------- + 3 files changed, 72 insertions(+), 31 deletions(-) + commit 9d3fd0598f27a318b29fb90446bb37bf7084380e Author: Brian Waldon Date: Thu May 24 06:21:17 2012 -0700 diff -Nru glance-2012.2~f2~20120524.1545/debian/changelog glance-2012.2~f2~20120524.1548/debian/changelog --- glance-2012.2~f2~20120524.1545/debian/changelog 2012-05-24 22:29:02.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/debian/changelog 2012-05-24 22:39:10.000000000 +0000 @@ -1,8 +1,8 @@ -glance (2012.2~f2~20120524.1545-0ubuntu0~precise152) precise; urgency=low +glance (2012.2~f2~20120524.1548-0ubuntu0~precise153) precise; urgency=low * Automated PPA build. Packaging revision: 78. - -- Soren Hansen Thu, 24 May 2012 22:29:02 +0000 + -- Soren Hansen Thu, 24 May 2012 22:39:10 +0000 glance (2012.1~e4~20120217.1275-0ubuntu2) UNRELEASED; urgency=low diff -Nru glance-2012.2~f2~20120524.1545/debian/patches/debian-changes-2012.2~f2~20120524.1545-0ubuntu0~precise152 glance-2012.2~f2~20120524.1548/debian/patches/debian-changes-2012.2~f2~20120524.1545-0ubuntu0~precise152 --- glance-2012.2~f2~20120524.1545/debian/patches/debian-changes-2012.2~f2~20120524.1545-0ubuntu0~precise152 2012-05-24 22:29:05.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/debian/patches/debian-changes-2012.2~f2~20120524.1545-0ubuntu0~precise152 1970-01-01 00:00:00.000000000 +0000 @@ -1,30 +0,0 @@ -Description: Upstream changes introduced in version 2012.2~f2~20120524.1545-0ubuntu0~precise152 - This patch has been created by dpkg-source during the package build. - Here's the last changelog entry, hopefully it gives details on why - those changes were made: - . - glance (2012.2~f2~20120524.1545-0ubuntu0~precise152) precise; urgency=low - . - * Automated PPA build. Packaging revision: 78. - . - The person named in the Author field signed this changelog entry. -Author: Soren Hansen - ---- -The information above should follow the Patch Tagging Guidelines, please -checkout http://dep.debian.net/deps/dep3/ to learn about the format. Here -are templates for supplementary fields that you might want to add: - -Origin: , -Bug: -Bug-Debian: http://bugs.debian.org/ -Bug-Ubuntu: https://launchpad.net/bugs/ -Forwarded: -Reviewed-By: -Last-Update: - ---- /dev/null -+++ glance-2012.2~f2~20120524.1545/.bzr-builddeb/default.conf -@@ -0,0 +1,2 @@ -+[BUILDDEB] -+merge = True diff -Nru glance-2012.2~f2~20120524.1545/debian/patches/debian-changes-2012.2~f2~20120524.1548-0ubuntu0~precise153 glance-2012.2~f2~20120524.1548/debian/patches/debian-changes-2012.2~f2~20120524.1548-0ubuntu0~precise153 --- glance-2012.2~f2~20120524.1545/debian/patches/debian-changes-2012.2~f2~20120524.1548-0ubuntu0~precise153 1970-01-01 00:00:00.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/debian/patches/debian-changes-2012.2~f2~20120524.1548-0ubuntu0~precise153 2012-05-24 22:39:12.000000000 +0000 @@ -0,0 +1,30 @@ +Description: Upstream changes introduced in version 2012.2~f2~20120524.1548-0ubuntu0~precise153 + This patch has been created by dpkg-source during the package build. + Here's the last changelog entry, hopefully it gives details on why + those changes were made: + . + glance (2012.2~f2~20120524.1548-0ubuntu0~precise153) precise; urgency=low + . + * Automated PPA build. Packaging revision: 78. + . + The person named in the Author field signed this changelog entry. +Author: Soren Hansen + +--- +The information above should follow the Patch Tagging Guidelines, please +checkout http://dep.debian.net/deps/dep3/ to learn about the format. Here +are templates for supplementary fields that you might want to add: + +Origin: , +Bug: +Bug-Debian: http://bugs.debian.org/ +Bug-Ubuntu: https://launchpad.net/bugs/ +Forwarded: +Reviewed-By: +Last-Update: + +--- /dev/null ++++ glance-2012.2~f2~20120524.1548/.bzr-builddeb/default.conf +@@ -0,0 +1,2 @@ ++[BUILDDEB] ++merge = True diff -Nru glance-2012.2~f2~20120524.1545/debian/patches/series glance-2012.2~f2~20120524.1548/debian/patches/series --- glance-2012.2~f2~20120524.1545/debian/patches/series 2012-05-24 22:29:05.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/debian/patches/series 2012-05-24 22:39:12.000000000 +0000 @@ -1,2 +1,2 @@ sql_conn.patch -debian-changes-2012.2~f2~20120524.1545-0ubuntu0~precise152 +debian-changes-2012.2~f2~20120524.1548-0ubuntu0~precise153 diff -Nru glance-2012.2~f2~20120524.1545/glance/api/v2/images.py glance-2012.2~f2~20120524.1548/glance/api/v2/images.py --- glance-2012.2~f2~20120524.1545/glance/api/v2/images.py 2012-05-24 22:25:30.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/glance/api/v2/images.py 2012-05-24 22:36:05.000000000 +0000 @@ -29,6 +29,19 @@ self.db_api = db_api or glance.registry.db.api self.db_api.configure_db(conf) + def _normalize_properties(self, image): + """Convert the properties from the stored format to a dict + + The db api returns a list of dicts that look like + {'name': , 'value': }, while it expects a format + like {: } in image create and update calls. This + function takes the extra step that the db api should be + responsible for in the image get calls. + """ + properties = [(p['name'], p['value']) for p in image['properties']] + image['properties'] = dict(properties) + return image + def create(self, req, image): if 'owner' not in image: image['owner'] = req.context.owner @@ -38,25 +51,29 @@ #TODO(bcwaldon): this should eventually be settable through the API image['status'] = 'queued' - return self.db_api.image_create(req.context, image) + image = self.db_api.image_create(req.context, image) + return self._normalize_properties(dict(image)) def index(self, req): #NOTE(bcwaldon): is_public=True gets public images and those # owned by the authenticated tenant filters = {'deleted': False, 'is_public': True} - return self.db_api.image_get_all(req.context, filters=filters) + images = self.db_api.image_get_all(req.context, filters=filters) + return [self._normalize_properties(dict(image)) for image in images] def show(self, req, image_id): try: - return self.db_api.image_get(req.context, image_id) + image = self.db_api.image_get(req.context, image_id) except (exception.NotFound, exception.Forbidden): raise webob.exc.HTTPNotFound() + return self._normalize_properties(dict(image)) def update(self, req, image_id, image): try: - return self.db_api.image_update(req.context, image_id, image) + image = self.db_api.image_update(req.context, image_id, image) except (exception.NotFound, exception.Forbidden): raise webob.exc.HTTPNotFound() + return self._normalize_properties(dict(image)) def delete(self, req, image_id): try: @@ -75,10 +92,20 @@ output = super(RequestDeserializer, self).default(request) body = output.pop('body') self.schema_api.validate('image', body) - output['image'] = body - if 'visibility' in body: - output['image']['is_public'] = body.pop('visibility') == 'public' - return output + + # Create a dict of base image properties, with user- and deployer- + # defined properties contained in a 'properties' dictionary + image = {'properties': body} + for key in ['id', 'name', 'visibility']: + try: + image[key] = image['properties'].pop(key) + except KeyError: + pass + + if 'visibility' in image: + image['is_public'] = image.pop('visibility') == 'public' + + return {'image': image} def create(self, request): return self._parse_image(request) @@ -107,11 +134,13 @@ def _filter_allowed_image_attributes(self, image): schema = self.schema_api.get_schema('image') + if schema.get('additionalProperties', True): + return dict(image.iteritems()) attrs = schema['properties'].keys() return dict((k, v) for (k, v) in image.iteritems() if k in attrs) def _format_image(self, image): - _image = dict(image['properties']) + _image = image['properties'] _image = self._filter_allowed_image_attributes(_image) for key in ['id', 'name']: diff -Nru glance-2012.2~f2~20120524.1545/glance/api/v2/router.py glance-2012.2~f2~20120524.1548/glance/api/v2/router.py --- glance-2012.2~f2~20120524.1545/glance/api/v2/router.py 2012-05-24 22:25:30.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/glance/api/v2/router.py 2012-05-24 22:36:05.000000000 +0000 @@ -39,7 +39,7 @@ self.conf = conf mapper = routes.Mapper() - schema_api = glance.schema.API() + schema_api = glance.schema.API(self.conf) glance.schema.load_custom_schema_properties(conf, schema_api) root_resource = root.create_resource(conf) diff -Nru glance-2012.2~f2~20120524.1545/glance/common/config.py glance-2012.2~f2~20120524.1548/glance/common/config.py --- glance-2012.2~f2~20120524.1545/glance/common/config.py 2012-05-24 22:25:30.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/glance/common/config.py 2012-05-24 22:36:05.000000000 +0000 @@ -36,6 +36,11 @@ cfg.StrOpt('flavor'), cfg.StrOpt('config_file'), ] +common_opts = [ + cfg.BoolOpt('allow_additional_image_properties', default=True, + help='Whether to allow users to specify image properties ' + 'beyond what the image schema provides'), +] class GlanceConfigOpts(cfg.CommonConfigOpts): @@ -46,6 +51,7 @@ version='%%prog %s' % version.version_string(), default_config_files=default_config_files, **kwargs) + self.register_opts(common_opts) self.default_paste_file = self.prog + '-paste.ini' diff -Nru glance-2012.2~f2~20120524.1545/glance/schema.py glance-2012.2~f2~20120524.1548/glance/schema.py --- glance-2012.2~f2~20120524.1545/glance/schema.py 2012-05-24 22:25:31.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/glance/schema.py 2012-05-24 22:36:05.000000000 +0000 @@ -58,15 +58,20 @@ class API(object): - def __init__(self, base_properties=_BASE_SCHEMA_PROPERTIES): + def __init__(self, conf, base_properties=_BASE_SCHEMA_PROPERTIES): + self.conf = conf self.base_properties = base_properties self.schema_properties = copy.deepcopy(self.base_properties) def get_schema(self, name): + if name == 'image' and self.conf.allow_additional_image_properties: + additional = {'type': 'string'} + else: + additional = False return { 'name': name, 'properties': self.schema_properties[name], - 'additionalProperties': False + 'additionalProperties': additional } def set_custom_schema_properties(self, schema_name, custom_properties): diff -Nru glance-2012.2~f2~20120524.1545/glance/tests/functional/test_schema.py glance-2012.2~f2~20120524.1548/glance/tests/functional/test_schema.py --- glance-2012.2~f2~20120524.1545/glance/tests/functional/test_schema.py 2012-05-24 22:25:31.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/glance/tests/functional/test_schema.py 2012-05-24 22:36:05.000000000 +0000 @@ -16,20 +16,23 @@ import unittest import glance.schema +import glance.tests.utils class TestSchemaAPI(unittest.TestCase): + def setUp(self): + conf = glance.tests.utils.TestConfigOpts() + self.schema_api = glance.schema.API(conf) + def test_load_image_schema(self): - schema_api = glance.schema.API() - output = schema_api.get_schema('image') + output = self.schema_api.get_schema('image') self.assertEqual('image', output['name']) expected_keys = set(['id', 'name', 'visibility']) self.assertEqual(expected_keys, set(output['properties'].keys())) def test_load_access_schema(self): - schema_api = glance.schema.API() - output = schema_api.get_schema('access') + output = self.schema_api.get_schema('access') self.assertEqual('access', output['name']) expected_keys = ['tenant_id', 'can_share'] self.assertEqual(expected_keys, output['properties'].keys()) diff -Nru glance-2012.2~f2~20120524.1545/glance/tests/functional/v2/test_images.py glance-2012.2~f2~20120524.1548/glance/tests/functional/v2/test_images.py --- glance-2012.2~f2~20120524.1545/glance/tests/functional/v2/test_images.py 2012-05-24 22:25:31.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/glance/tests/functional/v2/test_images.py 2012-05-24 22:36:05.000000000 +0000 @@ -59,10 +59,10 @@ images = json.loads(response.text)['images'] self.assertEqual(0, len(images)) - # Create an image + # Create an image (with a deployer-defined property) path = self._url('/images') headers = self._headers({'content-type': 'application/json'}) - data = json.dumps({'name': 'image-1'}) + data = json.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(200, response.status_code) image_location_header = response.headers['Location'] @@ -84,16 +84,21 @@ self.assertEqual(200, response.status_code) image = json.loads(response.text)['image'] self.assertEqual(image_id, image['id']) + self.assertEqual('bar', image['foo']) - # The image should be mutable + # The image should be mutable, including adding new properties path = self._url('/images/%s' % image_id) - data = json.dumps({'name': 'image-2'}) + data = json.dumps({'name': 'image-2', 'format': 'vhd', + 'foo': 'baz', 'ping': 'pong'}) response = requests.put(path, headers=self._headers(), data=data) self.assertEqual(200, response.status_code) # Returned image entity should reflect the changes image = json.loads(response.text)['image'] self.assertEqual('image-2', image['name']) + self.assertEqual('vhd', image['format']) + self.assertEqual('baz', image['foo']) + self.assertEqual('pong', image['ping']) # Updates should persist across requests path = self._url('/images/%s' % image_id) @@ -102,6 +107,8 @@ image = json.loads(response.text)['image'] self.assertEqual(image_id, image['id']) self.assertEqual('image-2', image['name']) + self.assertEqual('baz', image['foo']) + self.assertEqual('pong', image['ping']) # Try to download data before its uploaded path = self._url('/images/%s/file' % image_id) diff -Nru glance-2012.2~f2~20120524.1545/glance/tests/unit/test_schema.py glance-2012.2~f2~20120524.1548/glance/tests/unit/test_schema.py --- glance-2012.2~f2~20120524.1545/glance/tests/unit/test_schema.py 2012-05-24 22:25:32.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/glance/tests/unit/test_schema.py 2012-05-24 22:36:05.000000000 +0000 @@ -17,6 +17,7 @@ from glance.common import exception import glance.schema +from glance.tests import utils as test_utils FAKE_BASE_PROPERTIES = { @@ -33,12 +34,25 @@ 'required': True, }, }, + 'image': { + 'gazump': { + 'type': 'string', + 'description': 'overcharge; rip off', + 'required': False, + }, + 'cumulus': { + 'type': 'string', + 'description': 'a heap; pile', + 'required': True, + }, + }, } class TestSchemaAPI(unittest.TestCase): def setUp(self): - self.schema_api = glance.schema.API(FAKE_BASE_PROPERTIES) + self.conf = test_utils.TestConfigOpts() + self.schema_api = glance.schema.API(self.conf, FAKE_BASE_PROPERTIES) def test_get_schema(self): output = self.schema_api.get_schema('fake1') @@ -164,3 +178,67 @@ 'additionalProperties': False, } self.assertEqual(output, expected) + + def test_get_image_schema_with_additional_properties_disabled(self): + self.conf.allow_additional_image_properties = False + output = self.schema_api.get_schema('image') + expected = { + 'name': 'image', + 'properties': { + 'gazump': { + 'type': 'string', + 'description': 'overcharge; rip off', + 'required': False, + }, + 'cumulus': { + 'type': 'string', + 'description': 'a heap; pile', + 'required': True, + }, + }, + 'additionalProperties': False, + } + self.assertEqual(output, expected) + + def test_get_image_schema_with_additional_properties_enabled(self): + self.conf.allow_additional_image_properties = True + output = self.schema_api.get_schema('image') + expected = { + 'name': 'image', + 'properties': { + 'gazump': { + 'type': 'string', + 'description': 'overcharge; rip off', + 'required': False, + }, + 'cumulus': { + 'type': 'string', + 'description': 'a heap; pile', + 'required': True, + }, + }, + 'additionalProperties': {'type': 'string'}, + } + self.assertEqual(output, expected) + + def test_get_other_schema_with_additional_image_properties_enabled(self): + self.conf.allow_additional_image_properties = True + output = self.schema_api.get_schema('fake1') + expected = { + 'name': 'fake1', + 'properties': { + 'id': { + 'type': 'string', + 'description': 'An identifier for the image', + 'required': False, + 'maxLength': 36, + }, + 'name': { + 'type': 'string', + 'description': 'Descriptive name for the image', + 'required': True, + }, + }, + 'additionalProperties': False, + } + self.assertEqual(output, expected) diff -Nru glance-2012.2~f2~20120524.1545/glance/tests/unit/v2/test_image_access_resource.py glance-2012.2~f2~20120524.1548/glance/tests/unit/v2/test_image_access_resource.py --- glance-2012.2~f2~20120524.1545/glance/tests/unit/v2/test_image_access_resource.py 2012-05-24 22:25:32.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/glance/tests/unit/v2/test_image_access_resource.py 2012-05-24 22:36:05.000000000 +0000 @@ -23,6 +23,7 @@ from glance.common import utils import glance.schema import glance.tests.unit.utils as test_utils +import glance.tests.utils class TestImageAccessController(unittest.TestCase): @@ -109,7 +110,8 @@ class TestImageAccessDeserializer(unittest.TestCase): def setUp(self): - schema_api = glance.schema.API() + conf = glance.tests.utils.TestConfigOpts() + schema_api = glance.schema.API(conf) self.deserializer = image_access.RequestDeserializer({}, schema_api) def test_create(self): @@ -131,7 +133,8 @@ class TestImageAccessDeserializerWithExtendedSchema(unittest.TestCase): def setUp(self): - schema_api = glance.schema.API() + conf = glance.tests.utils.TestConfigOpts() + schema_api = glance.schema.API(conf) props = { 'color': { 'type': 'string', diff -Nru glance-2012.2~f2~20120524.1545/glance/tests/unit/v2/test_images_resource.py glance-2012.2~f2~20120524.1548/glance/tests/unit/v2/test_images_resource.py --- glance-2012.2~f2~20120524.1545/glance/tests/unit/v2/test_images_resource.py 2012-05-24 22:25:32.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/glance/tests/unit/v2/test_images_resource.py 2012-05-24 22:36:05.000000000 +0000 @@ -23,6 +23,7 @@ from glance.common import utils import glance.schema import glance.tests.unit.utils as test_utils +import glance.tests.utils class TestImagesController(unittest.TestCase): @@ -64,7 +65,7 @@ 'location': None, 'status': 'queued', 'is_public': False, - 'properties': [], + 'properties': {}, } self.assertEqual(expected, output) @@ -85,7 +86,7 @@ 'location': None, 'status': 'queued', 'is_public': True, - 'properties': [], + 'properties': {}, } self.assertEqual(expected, output) @@ -100,7 +101,7 @@ 'location': test_utils.UUID1, 'status': 'queued', 'is_public': False, - 'properties': [], + 'properties': {}, } self.assertEqual(expected, output) @@ -113,7 +114,8 @@ class TestImagesDeserializer(unittest.TestCase): def setUp(self): - schema_api = glance.schema.API() + self.conf = glance.tests.utils.TestConfigOpts() + schema_api = glance.schema.API(self.conf) self.deserializer = glance.api.v2.images.RequestDeserializer( {}, schema_api) @@ -122,41 +124,48 @@ image_id = utils.generate_uuid() request.body = json.dumps({'id': image_id}) output = self.deserializer.create(request) - expected = {'image': {'id': image_id}} + expected = {'image': {'id': image_id, 'properties': {}}} self.assertEqual(expected, output) def test_create_with_name(self): request = test_utils.FakeRequest() request.body = json.dumps({'name': 'image-1'}) output = self.deserializer.create(request) - expected = {'image': {'name': 'image-1'}} + expected = {'image': {'name': 'image-1', 'properties': {}}} self.assertEqual(expected, output) def test_create_public(self): request = test_utils.FakeRequest() request.body = json.dumps({'visibility': 'public'}) output = self.deserializer.create(request) - expected = {'image': {'is_public': True}} + expected = {'image': {'is_public': True, 'properties': {}}} self.assertEqual(expected, output) def test_create_private(self): request = test_utils.FakeRequest() request.body = json.dumps({'visibility': 'private'}) output = self.deserializer.create(request) - expected = {'image': {'is_public': False}} + expected = {'image': {'is_public': False, 'properties': {}}} self.assertEqual(expected, output) def test_update(self): request = test_utils.FakeRequest() request.body = json.dumps({'name': 'image-1', 'visibility': 'public'}) output = self.deserializer.update(request) - expected = {'image': {'name': 'image-1', 'is_public': True}} + expected = { + 'image': { + 'name': 'image-1', + 'is_public': True, + 'properties': {}, + }, + } self.assertEqual(expected, output) class TestImagesDeserializerWithExtendedSchema(unittest.TestCase): def setUp(self): - schema_api = glance.schema.API() + conf = glance.tests.utils.TestConfigOpts() + schema_api = glance.schema.API(conf) props = { 'pants': { 'type': 'string', @@ -172,7 +181,12 @@ request = test_utils.FakeRequest() request.body = json.dumps({'name': 'image-1', 'pants': 'on'}) output = self.deserializer.create(request) - expected = {'image': {'name': 'image-1', 'pants': 'on'}} + expected = { + 'image': { + 'name': 'image-1', + 'properties': {'pants': 'on'}, + }, + } self.assertEqual(expected, output) def test_create_bad_data(self): @@ -185,7 +199,12 @@ request = test_utils.FakeRequest() request.body = json.dumps({'name': 'image-1', 'pants': 'off'}) output = self.deserializer.update(request) - expected = {'image': {'name': 'image-1', 'pants': 'off'}} + expected = { + 'image': { + 'name': 'image-1', + 'properties': {'pants': 'off'}, + }, + } self.assertEqual(expected, output) def test_update_bad_data(self): @@ -195,9 +214,59 @@ self.deserializer.update, request) +class TestImagesDeserializerWithAdditionalProperties(unittest.TestCase): + def setUp(self): + self.conf = glance.tests.utils.TestConfigOpts() + self.conf.allow_additional_image_properties = True + schema_api = glance.schema.API(self.conf) + self.deserializer = glance.api.v2.images.RequestDeserializer( + {}, schema_api) + + def test_create(self): + request = test_utils.FakeRequest() + request.body = json.dumps({'foo': 'bar'}) + output = self.deserializer.create(request) + expected = {'image': {'properties': {'foo': 'bar'}}} + self.assertEqual(expected, output) + + def test_create_with_additional_properties_disallowed(self): + self.conf.allow_additional_image_properties = False + request = test_utils.FakeRequest() + request.body = json.dumps({'foo': 'bar'}) + self.assertRaises(exception.InvalidObject, + self.deserializer.create, request) + + def test_create_with_numeric_property(self): + request = test_utils.FakeRequest() + request.body = json.dumps({'abc': 123}) + self.assertRaises(exception.InvalidObject, + self.deserializer.create, request) + + def test_create_with_list_property(self): + request = test_utils.FakeRequest() + request.body = json.dumps({'foo': ['bar']}) + self.assertRaises(exception.InvalidObject, + self.deserializer.create, request) + + def test_update(self): + request = test_utils.FakeRequest() + request.body = json.dumps({'foo': 'bar'}) + output = self.deserializer.update(request) + expected = {'image': {'properties': {'foo': 'bar'}}} + self.assertEqual(expected, output) + + def test_update_with_additional_properties_disallowed(self): + self.conf.allow_additional_image_properties = False + request = test_utils.FakeRequest() + request.body = json.dumps({'foo': 'bar'}) + self.assertRaises(exception.InvalidObject, + self.deserializer.update, request) + + class TestImagesSerializer(unittest.TestCase): def setUp(self): - schema_api = glance.schema.API() + conf = glance.tests.utils.TestConfigOpts() + schema_api = glance.schema.API(conf) self.serializer = glance.api.v2.images.ResponseSerializer(schema_api) def test_index(self): @@ -206,13 +275,13 @@ 'id': test_utils.UUID1, 'name': 'image-1', 'is_public': True, - 'properties': [], + 'properties': {}, }, { 'id': test_utils.UUID2, 'name': 'image-2', 'is_public': False, - 'properties': [], + 'properties': {}, }, ] expected = { @@ -261,7 +330,7 @@ 'id': test_utils.UUID2, 'name': 'image-2', 'is_public': True, - 'properties': [], + 'properties': {}, } expected = { 'image': { @@ -290,7 +359,7 @@ 'id': test_utils.UUID2, 'name': 'image-2', 'is_public': False, - 'properties': [], + 'properties': {}, } self_link = '/v2/images/%s' % test_utils.UUID2 expected = { @@ -315,7 +384,7 @@ 'id': test_utils.UUID2, 'name': 'image-2', 'is_public': True, - 'properties': [], + 'properties': {}, } self_link = '/v2/images/%s' % test_utils.UUID2 expected = { @@ -337,7 +406,9 @@ class TestImagesSerializerWithExtendedSchema(unittest.TestCase): def setUp(self): - self.schema_api = glance.schema.API() + self.conf = glance.tests.utils.TestConfigOpts() + self.conf.allow_additional_image_properties = False + self.schema_api = glance.schema.API(self.conf) props = { 'color': { 'type': 'string', @@ -350,10 +421,7 @@ 'id': test_utils.UUID2, 'name': 'image-2', 'is_public': False, - 'properties': { - 'color': 'green', - 'mood': 'grouchy', - }, + 'properties': {'color': 'green', 'mood': 'grouchy'}, } def test_show(self): @@ -406,3 +474,97 @@ response = webob.Response() serializer.show(response, self.fixture) self.assertEqual(expected, json.loads(response.body)) + + +class TestImagesSerializerWithAdditionalProperties(unittest.TestCase): + def setUp(self): + self.conf = glance.tests.utils.TestConfigOpts() + self.conf.allow_additional_image_properties = True + self.schema_api = glance.schema.API(self.conf) + self.fixture = { + 'id': test_utils.UUID2, + 'name': 'image-2', + 'is_public': False, + 'properties': { + 'marx': 'groucho', + }, + } + + def test_show(self): + serializer = glance.api.v2.images.ResponseSerializer(self.schema_api) + expected = { + 'image': { + 'id': test_utils.UUID2, + 'name': 'image-2', + 'visibility': 'private', + 'marx': 'groucho', + 'links': [ + { + 'rel': 'self', + 'href': '/v2/images/%s' % test_utils.UUID2, + }, + { + 'rel': 'file', + 'href': '/v2/images/%s/file' % test_utils.UUID2, + }, + {'rel': 'describedby', 'href': '/v2/schemas/image'} + ], + }, + } + response = webob.Response() + serializer.show(response, self.fixture) + self.assertEqual(expected, json.loads(response.body)) + + def test_show_invalid_additional_property(self): + """Ensure that the serializer passes through invalid additional + properties (i.e. non-string) without complaining. + """ + serializer = glance.api.v2.images.ResponseSerializer(self.schema_api) + self.fixture['properties']['marx'] = 123 + expected = { + 'image': { + 'id': test_utils.UUID2, + 'name': 'image-2', + 'visibility': 'private', + 'marx': 123, + 'links': [ + { + 'rel': 'self', + 'href': '/v2/images/%s' % test_utils.UUID2, + }, + { + 'rel': 'file', + 'href': '/v2/images/%s/file' % test_utils.UUID2, + }, + {'rel': 'describedby', 'href': '/v2/schemas/image'} + ], + }, + } + response = webob.Response() + serializer.show(response, self.fixture) + self.assertEqual(expected, json.loads(response.body)) + + def test_show_with_additional_properties_disabled(self): + self.conf.allow_additional_image_properties = False + serializer = glance.api.v2.images.ResponseSerializer(self.schema_api) + expected = { + 'image': { + 'id': test_utils.UUID2, + 'name': 'image-2', + 'visibility': 'private', + 'links': [ + { + 'rel': 'self', + 'href': '/v2/images/%s' % test_utils.UUID2, + }, + { + 'rel': 'file', + 'href': '/v2/images/%s/file' % test_utils.UUID2, + }, + {'rel': 'describedby', 'href': '/v2/schemas/image'} + ], + }, + } + response = webob.Response() + serializer.show(response, self.fixture) + self.assertEqual(expected, json.loads(response.body)) diff -Nru glance-2012.2~f2~20120524.1545/glance/tests/unit/v2/test_schemas_resource.py glance-2012.2~f2~20120524.1548/glance/tests/unit/v2/test_schemas_resource.py --- glance-2012.2~f2~20120524.1545/glance/tests/unit/v2/test_schemas_resource.py 2012-05-24 22:25:32.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/glance/tests/unit/v2/test_schemas_resource.py 2012-05-24 22:36:05.000000000 +0000 @@ -16,15 +16,17 @@ import unittest from glance.api.v2 import schemas -import glance.tests.unit.utils as test_utils import glance.schema +import glance.tests.unit.utils as test_utils +import glance.tests.utils class TestSchemasController(unittest.TestCase): def setUp(self): super(TestSchemasController, self).setUp() - self.schema_api = glance.schema.API() + conf = glance.tests.utils.TestConfigOpts() + self.schema_api = glance.schema.API(conf) self.controller = schemas.Controller({}, self.schema_api) def test_index(self): diff -Nru glance-2012.2~f2~20120524.1545/glance/vcsversion.py glance-2012.2~f2~20120524.1548/glance/vcsversion.py --- glance-2012.2~f2~20120524.1545/glance/vcsversion.py 2012-05-24 22:27:27.000000000 +0000 +++ glance-2012.2~f2~20120524.1548/glance/vcsversion.py 2012-05-24 22:37:41.000000000 +0000 @@ -2,6 +2,6 @@ # This file is automatically generated by setup.py, So don't edit it. :) version_info = { 'branch_nick': '(no', - 'revision_id': '26ce3e0e4cd94d57f3f4c2aaaf87b35c0643cc21', - 'revno': 1545 + 'revision_id': '9a838a8b34e435223e1e01d73d84665e6e8f7102', + 'revno': 1548 }