Allow None values to be returned from the API

Currently, Glance's API v2 doesn't return fields whose value is None.
This, unfortunately, is wrong for a client perspective since it would
create inconsistencies between calls and images due to the lack of
fields in the response.

The API should guarantee consistency in its replies and ensure all
fields have a value, even if it's None.

NOTE: This work is part of the migration to v2. It fixes inconsistencies
in the API and improves the interaction between the client library and
Glance.

NOTE2: A follow-up patch will bump the minor API version, wait for it.

ApiImpact
DocImpact
Closes-bug: #1398314

Change-Id: Ieaddd8a686cf7361f18cb1ee83b7887cdca22bd6
This commit is contained in:
Flavio Percoco 2014-12-01 21:52:22 +01:00
parent 5a265dada4
commit d17a1ed78a
4 changed files with 54 additions and 8 deletions

View File

@ -45,7 +45,7 @@ class Schema(object):
def filter(self, obj): def filter(self, obj):
filtered = {} filtered = {}
for key, value in six.iteritems(obj): for key, value in six.iteritems(obj):
if self._filter_func(self.properties, key) and value is not None: if self._filter_func(self.properties, key):
filtered[key] = value filtered[key] = value
return filtered return filtered

View File

@ -132,6 +132,9 @@ class TestImages(functional.FunctionalTest):
u'disk_format', u'disk_format',
u'container_format', u'container_format',
u'owner', u'owner',
u'checksum',
u'size',
u'virtual_size',
]) ])
self.assertEqual(checked_keys, set(image.keys())) self.assertEqual(checked_keys, set(image.keys()))
expected_image = { expected_image = {
@ -192,6 +195,9 @@ class TestImages(functional.FunctionalTest):
u'disk_format', u'disk_format',
u'container_format', u'container_format',
u'owner', u'owner',
u'checksum',
u'size',
u'virtual_size',
]) ])
self.assertEqual(checked_keys, set(image.keys())) self.assertEqual(checked_keys, set(image.keys()))
expected_image = { expected_image = {
@ -279,9 +285,9 @@ class TestImages(functional.FunctionalTest):
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
image = jsonutils.loads(response.text) image = jsonutils.loads(response.text)
self.assertEqual(image_id, image['id']) self.assertEqual(image_id, image['id'])
self.assertFalse('checksum' in image) self.assertIsNone(image['checksum'])
self.assertNotIn('size', image) self.assertIsNone(image['size'])
self.assertNotIn('virtual_size', image) self.assertIsNone(image['virtual_size'])
self.assertEqual('bar', image['foo']) self.assertEqual('bar', image['foo'])
self.assertFalse(image['protected']) self.assertFalse(image['protected'])
self.assertEqual('kernel', image['type']) self.assertEqual('kernel', image['type'])
@ -456,8 +462,8 @@ class TestImages(functional.FunctionalTest):
response = requests.patch(path, headers=headers, data=data) response = requests.patch(path, headers=headers, data=data)
self.assertEqual(200, response.status_code, response.text) self.assertEqual(200, response.status_code, response.text)
image = jsonutils.loads(response.text) image = jsonutils.loads(response.text)
self.assertNotIn('size', image) self.assertIsNone(image['size'])
self.assertNotIn('virtual_size', image) self.assertIsNone(image['virtual_size'])
self.assertEqual('queued', image['status']) self.assertEqual('queued', image['status'])
# Deletion should work. Deleting image-1 # Deletion should work. Deleting image-1
@ -2111,8 +2117,8 @@ class TestImages(functional.FunctionalTest):
image = jsonutils.loads(response.text) image = jsonutils.loads(response.text)
image_id = image['id'] image_id = image['id']
self.assertEqual('queued', image['status']) self.assertEqual('queued', image['status'])
self.assertNotIn('size', image) self.assertIsNone(image['size'])
self.assertNotIn('virtual_size', image) self.assertIsNone(image['virtual_size'])
file_path = os.path.join(self.test_dir, 'fake_image') file_path = os.path.join(self.test_dir, 'fake_image')
with open(file_path, 'w') as fap: with open(file_path, 'w') as fap:

View File

@ -92,6 +92,7 @@ class TestTasks(functional.FunctionalTest):
u'self', u'self',
u'status', u'status',
u'type', u'type',
u'result',
u'updated_at']) u'updated_at'])
self.assertEqual(checked_keys, set(task.keys())) self.assertEqual(checked_keys, set(task.keys()))
expected_task = { expected_task = {

View File

@ -2743,6 +2743,16 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'self': '/v2/images/%s' % UUID2, 'self': '/v2/images/%s' % UUID2,
'file': '/v2/images/%s/file' % UUID2, 'file': '/v2/images/%s/file' % UUID2,
'schema': '/v2/schemas/image', 'schema': '/v2/schemas/image',
'size': None,
'name': None,
'owner': None,
'min_ram': None,
'min_disk': None,
'checksum': None,
'disk_format': None,
'virtual_size': None,
'container_format': None,
}, },
], ],
'first': '/v2/images', 'first': '/v2/images',
@ -2847,6 +2857,15 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'self': '/v2/images/%s' % UUID2, 'self': '/v2/images/%s' % UUID2,
'file': '/v2/images/%s/file' % UUID2, 'file': '/v2/images/%s/file' % UUID2,
'schema': '/v2/schemas/image', 'schema': '/v2/schemas/image',
'size': None,
'name': None,
'owner': None,
'min_ram': None,
'min_disk': None,
'checksum': None,
'disk_format': None,
'virtual_size': None,
'container_format': None,
} }
response = webob.Response() response = webob.Response()
self.serializer.show(response, self.fixtures[1]) self.serializer.show(response, self.fixtures[1])
@ -3113,6 +3132,10 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
'self': '/v2/images/%s' % UUID2, 'self': '/v2/images/%s' % UUID2,
'file': '/v2/images/%s/file' % UUID2, 'file': '/v2/images/%s/file' % UUID2,
'schema': '/v2/schemas/image', 'schema': '/v2/schemas/image',
'min_ram': None,
'min_disk': None,
'disk_format': None,
'container_format': None,
} }
response = webob.Response() response = webob.Response()
self.serializer.show(response, self.fixture) self.serializer.show(response, self.fixture)
@ -3137,6 +3160,10 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
'self': '/v2/images/%s' % UUID2, 'self': '/v2/images/%s' % UUID2,
'file': '/v2/images/%s/file' % UUID2, 'file': '/v2/images/%s/file' % UUID2,
'schema': '/v2/schemas/image', 'schema': '/v2/schemas/image',
'min_ram': None,
'min_disk': None,
'disk_format': None,
'container_format': None,
} }
response = webob.Response() response = webob.Response()
self.serializer.show(response, self.fixture) self.serializer.show(response, self.fixture)
@ -3173,6 +3200,10 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
'file': '/v2/images/%s/file' % UUID2, 'file': '/v2/images/%s/file' % UUID2,
'schema': '/v2/schemas/image', 'schema': '/v2/schemas/image',
'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81', 'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81',
'min_ram': None,
'min_disk': None,
'disk_format': None,
'container_format': None,
} }
response = webob.Response() response = webob.Response()
serializer.show(response, self.fixture) serializer.show(response, self.fixture)
@ -3203,6 +3234,10 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
'file': '/v2/images/%s/file' % UUID2, 'file': '/v2/images/%s/file' % UUID2,
'schema': '/v2/schemas/image', 'schema': '/v2/schemas/image',
'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81', 'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81',
'min_ram': None,
'min_disk': None,
'disk_format': None,
'container_format': None,
} }
response = webob.Response() response = webob.Response()
serializer.show(response, self.fixture) serializer.show(response, self.fixture)
@ -3227,6 +3262,10 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
'self': '/v2/images/%s' % UUID2, 'self': '/v2/images/%s' % UUID2,
'file': '/v2/images/%s/file' % UUID2, 'file': '/v2/images/%s/file' % UUID2,
'schema': '/v2/schemas/image', 'schema': '/v2/schemas/image',
'min_ram': None,
'min_disk': None,
'disk_format': None,
'container_format': None,
} }
response = webob.Response() response = webob.Response()
serializer.show(response, self.fixture) serializer.show(response, self.fixture)