Add `virtual_size` to Glance's API v2

This patch adds the knowledge of a virtual_size field to Glance's API
v2. The virtual_size field should respect the same rules applied to the
size field in terms of readability, access control and propagation.

Glance's API v1 has been left unmodified.

docImpact
Implements blueprint: split-image-size

Change-Id: Ie4f58ee2e4da3a6c1229840295c7f62023a95b70
This commit is contained in:
Flavio Percoco 2014-02-11 14:23:30 +01:00
parent 1679422e0a
commit e98576efae
12 changed files with 104 additions and 30 deletions

View File

@ -284,6 +284,7 @@ class ImmutableImageProxy(object):
disk_format = _immutable_attr('base', 'disk_format')
container_format = _immutable_attr('base', 'container_format')
size = _immutable_attr('base', 'size')
virtual_size = _immutable_attr('base', 'virtual_size')
extra_properties = _immutable_attr('base', 'extra_properties',
proxy=ImmutableProperties)
tags = _immutable_attr('base', 'tags', proxy=ImmutableTags)

View File

@ -268,13 +268,14 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
_disallowed_properties = ['direct_url', 'self', 'file', 'schema']
_readonly_properties = ['created_at', 'updated_at', 'status', 'checksum',
'size', 'direct_url', 'self', 'file', 'schema']
'size', 'virtual_size', 'direct_url', 'self',
'file', 'schema']
_reserved_properties = ['owner', 'is_public', 'location', 'deleted',
'deleted_at']
_base_properties = ['checksum', 'created_at', 'container_format',
'disk_format', 'id', 'min_disk', 'min_ram', 'name',
'size', 'status', 'tags', 'updated_at', 'visibility',
'protected']
'size', 'virtual_size', 'status', 'tags',
'updated_at', 'visibility', 'protected']
_path_depth_limits = {'locations': {'add': 2, 'remove': 2, 'replace': 1}}
def __init__(self, schema=None):
@ -565,8 +566,9 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
try:
image_view = dict(image.extra_properties)
attributes = ['name', 'disk_format', 'container_format',
'visibility', 'size', 'status', 'checksum',
'protected', 'min_ram', 'min_disk', 'owner']
'visibility', 'size', 'virtual_size', 'status',
'checksum', 'protected', 'min_ram', 'min_disk',
'owner']
for key in attributes:
image_view[key] = getattr(image, key)
image_view['id'] = image.image_id
@ -678,6 +680,10 @@ def _get_base_properties():
'type': 'integer',
'description': _('Size of image file in bytes (READ-ONLY)'),
},
'virtual_size': {
'type': 'integer',
'description': _('Virtual size of image in bytes (READ-ONLY)'),
},
'container_format': {
'type': 'string',
'description': _('Format of the container'),

View File

@ -43,7 +43,7 @@ BASE_MODEL_ATTRS = set(['id', 'created_at', 'updated_at', 'deleted_at',
'deleted'])
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size',
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size', 'virtual_size',
'disk_format', 'container_format',
'min_disk', 'min_ram', 'is_public',
'locations', 'checksum', 'owner',
@ -112,6 +112,7 @@ class ImageRepo(object):
disk_format=db_image['disk_format'],
container_format=db_image['container_format'],
size=db_image['size'],
virtual_size=db_image['virtual_size'],
extra_properties=properties,
tags=db_tags
)
@ -139,6 +140,7 @@ class ImageRepo(object):
'disk_format': image.disk_format,
'container_format': image.container_format,
'size': image.size,
'virtual_size': image.virtual_size,
'is_public': image.visibility == 'public',
'properties': dict(image.extra_properties),
}

View File

@ -44,7 +44,7 @@ def _import_delayed_delete():
class ImageFactory(object):
_readonly_properties = ['created_at', 'updated_at', 'status', 'checksum',
'size']
'size', 'virtual_size']
_reserved_properties = ['owner', 'is_public', 'locations',
'deleted', 'deleted_at', 'direct_url', 'self',
'file', 'schema']
@ -121,6 +121,7 @@ class Image(object):
self._disk_format = kwargs.pop('disk_format', None)
self._container_format = kwargs.pop('container_format', None)
self.size = kwargs.pop('size', None)
self.virtual_size = kwargs.pop('virtual_size', None)
extra_properties = kwargs.pop('extra_properties', None) or {}
self.extra_properties = ExtraProperties(extra_properties)
self.tags = kwargs.pop('tags', None) or []
@ -157,6 +158,7 @@ class Image(object):
# status is updated to 'queued'
if status == 'queued':
self.size = None
self.virtual_size = None
self._status = status
@property

View File

@ -117,6 +117,7 @@ class Image(object):
disk_format = _proxy('base', 'disk_format')
container_format = _proxy('base', 'container_format')
size = _proxy('base', 'size')
virtual_size = _proxy('base', 'virtual_size')
extra_properties = _proxy('base', 'extra_properties')
tags = _proxy('base', 'tags')

View File

@ -233,7 +233,8 @@ class TestImages(functional.FunctionalTest):
image = jsonutils.loads(response.text)
self.assertEqual(image_id, image['id'])
self.assertFalse('checksum' in image)
self.assertFalse('size' in image)
self.assertNotIn('size', image)
self.assertNotIn('virtual_size', image)
self.assertEqual('bar', image['foo'])
self.assertEqual(False, image['protected'])
self.assertEqual('kernel', image['type'])
@ -408,7 +409,8 @@ class TestImages(functional.FunctionalTest):
response = requests.patch(path, headers=headers, data=data)
self.assertEqual(200, response.status_code, response.text)
image = jsonutils.loads(response.text)
self.assertTrue('size' not in image)
self.assertNotIn('size', image)
self.assertNotIn('virtual_size', image)
self.assertEqual('queued', image['status'])
# Deletion should work. Deleting image-1
@ -1765,6 +1767,7 @@ class TestImages(functional.FunctionalTest):
image_id = image['id']
self.assertEqual(image['status'], 'queued')
self.assertNotIn('size', image)
self.assertNotIn('virtual_size', image)
file_path = os.path.join(self.test_dir, 'fake_image')
with open(file_path, 'w') as fap:

View File

@ -41,6 +41,7 @@ class TestSchemas(functional.FunctionalTest):
'updated_at',
'tags',
'size',
'virtual_size',
'owner',
'container_format',
'disk_format',

View File

@ -285,6 +285,7 @@ class TestCacheMiddlewareProcessRequest(base.IsolatedUnitTest):
'disk_format': 'raw',
'container_format': 'bare',
'size': '123456789',
'virtual_size': '123456789',
'is_public': 'public',
'deleted': False,
'updated_at': '',

View File

@ -37,12 +37,13 @@ class Raise(object):
class FakeImage(object):
def __init__(self, image_id=None, data=None, checksum=None, size=0,
locations=None, container_format='bear', disk_format='rawr',
status=None):
virtual_size=0, locations=None, container_format='bear',
disk_format='rawr', status=None):
self.image_id = image_id
self.data = data
self.checksum = checksum
self.size = size
self.virtual_size = virtual_size
self.locations = locations
self.container_format = container_format
self.disk_format = disk_format
@ -250,6 +251,7 @@ class TestImagesController(base.StoreClearingUnitTest):
prepare_payload = output['meta'].copy()
prepare_payload['checksum'] = None
prepare_payload['size'] = None
prepare_payload['virtual_size'] = None
prepare_payload['location'] = None
prepare_payload['status'] = 'queued'
del prepare_payload['updated_at']

View File

@ -63,6 +63,7 @@ def _db_fixture(id, **kwargs):
'status': 'queued',
'tags': [],
'size': None,
'virtual_size': None,
'locations': [],
'protected': False,
'disk_format': None,
@ -84,6 +85,7 @@ def _domain_fixture(id, **kwargs):
'owner': None,
'status': 'queued',
'size': None,
'virtual_size': None,
'locations': [],
'protected': False,
'disk_format': None,
@ -127,7 +129,7 @@ class TestImagesController(base.IsolatedUnitTest):
self.db.reset()
self.images = [
_db_fixture(UUID1, owner=TENANT1, checksum=CHKSUM,
name='1', size=256,
name='1', size=256, virtual_size=1024,
is_public=True,
locations=[{'url': '%s/%s' % (BASE_URI, UUID1),
'metadata': {}}],
@ -135,7 +137,7 @@ class TestImagesController(base.IsolatedUnitTest):
container_format='bare',
status='active'),
_db_fixture(UUID2, owner=TENANT1, checksum=CHKSUM1,
name='2', size=512,
name='2', size=512, virtual_size=2048,
is_public=True,
disk_format='raw',
container_format='bare',
@ -144,9 +146,10 @@ class TestImagesController(base.IsolatedUnitTest):
properties={'hypervisor_type': 'kvm', 'foo': 'bar',
'bar': 'foo'}),
_db_fixture(UUID3, owner=TENANT3, checksum=CHKSUM1,
name='3', size=512, is_public=True,
tags=['windows', '64bit', 'x86']),
_db_fixture(UUID4, owner=TENANT4, name='4', size=1024),
name='3', size=512, virtual_size=2048,
is_public=True, tags=['windows', '64bit', 'x86']),
_db_fixture(UUID4, owner=TENANT4, name='4',
size=1024, virtual_size=3072),
]
[self.db.image_create(None, image) for image in self.images]
@ -288,6 +291,37 @@ class TestImagesController(base.IsolatedUnitTest):
expected = set([UUID2, UUID3])
self.assertEqual(actual, expected)
def test_index_virtual_size_max_filter(self):
ref = '/images?virtual_size_max=2048'
request = unit_test_utils.get_fake_request(ref)
output = self.controller.index(request,
filters={'virtual_size_max': 2048})
self.assertEqual(3, len(output['images']))
actual = set([image.image_id for image in output['images']])
expected = set([UUID1, UUID2, UUID3])
self.assertEqual(actual, expected)
def test_index_virtual_size_min_filter(self):
ref = '/images?virtual_size_min=2048'
request = unit_test_utils.get_fake_request(ref)
output = self.controller.index(request,
filters={'virtual_size_min': 2048})
self.assertEqual(2, len(output['images']))
actual = set([image.image_id for image in output['images']])
expected = set([UUID2, UUID3])
self.assertEqual(actual, expected)
def test_index_virtual_size_range_filter(self):
path = '/images?virtual_size_min=512&virtual_size_max=2048'
request = unit_test_utils.get_fake_request(path)
output = self.controller.index(request,
filters={'virtual_size_min': 2048,
'virtual_size_max': 2048})
self.assertEqual(2, len(output['images']))
actual = set([image.image_id for image in output['images']])
expected = set([UUID2, UUID3])
self.assertEqual(actual, expected)
def test_index_with_invalid_max_range_filter_value(self):
request = unit_test_utils.get_fake_request('/images?size_max=blah')
self.assertRaises(webob.exc.HTTPBadRequest,
@ -1921,6 +1955,7 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
#{'status': 'saving'},
{'direct_url': 'http://example.com'},
#{'size': 10},
#{'virtual_size': 10},
#{'checksum': 'asdf'},
{'self': 'http://example.com'},
{'file': 'http://example.com'},
@ -2126,6 +2161,7 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
'status': 'active',
'checksum': 'abcdefghijklmnopqrstuvwxyz012345',
'size': 9001,
'virtual_size': 9001,
'created_at': ISOTIME,
'updated_at': ISOTIME,
}
@ -2519,10 +2555,11 @@ class TestImagesSerializer(test_utils.BaseTestCase):
self.fixtures = [
#NOTE(bcwaldon): This first fixture has every property defined
_domain_fixture(UUID1, name='image-1', size=1024,
created_at=DATETIME, updated_at=DATETIME,
owner=TENANT1, visibility='public',
container_format='ami', tags=['one', 'two'],
disk_format='ami', min_ram=128, min_disk=10,
virtual_size=3072, created_at=DATETIME,
updated_at=DATETIME, owner=TENANT1,
visibility='public', container_format='ami',
tags=['one', 'two'], disk_format='ami',
min_ram=128, min_disk=10,
checksum='ca425b88f047ce8ec45ee90e813ada91'),
#NOTE(bcwaldon): This second fixture depends on default behavior
@ -2541,6 +2578,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'protected': False,
'tags': set(['one', 'two']),
'size': 1024,
'virtual_size': 3072,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
'container_format': 'ami',
'disk_format': 'ami',
@ -2630,6 +2668,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'protected': False,
'tags': set(['one', 'two']),
'size': 1024,
'virtual_size': 3072,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
'container_format': 'ami',
'disk_format': 'ami',
@ -2675,6 +2714,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'protected': False,
'tags': ['two', 'one'],
'size': 1024,
'virtual_size': 3072,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
'container_format': 'ami',
'disk_format': 'ami',
@ -2703,6 +2743,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'protected': False,
'tags': set(['one', 'two']),
'size': 1024,
'virtual_size': 3072,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
'container_format': 'ami',
'disk_format': 'ami',
@ -2733,6 +2774,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
_domain_fixture(UUID1, **{
'name': u'OpenStack\u2122-1',
'size': 1024,
'virtual_size': 3072,
'tags': [u'\u2160', u'\u2161'],
'created_at': DATETIME,
'updated_at': DATETIME,
@ -2759,6 +2801,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
u'protected': False,
u'tags': [u'\u2161', u'\u2160'],
u'size': 1024,
u'virtual_size': 3072,
u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
u'container_format': u'ami',
u'disk_format': u'ami',
@ -2793,6 +2836,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
u'protected': False,
u'tags': set([u'\u2160', u'\u2161']),
u'size': 1024,
u'virtual_size': 3072,
u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
u'container_format': u'ami',
u'disk_format': u'ami',
@ -2823,6 +2867,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
u'protected': False,
u'tags': [u'\u2161', u'\u2160'],
u'size': 1024,
u'virtual_size': 3072,
u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
u'container_format': u'ami',
u'disk_format': u'ami',
@ -2853,6 +2898,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
u'protected': False,
u'tags': set([u'\u2160', u'\u2161']),
u'size': 1024,
u'virtual_size': 3072,
u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
u'container_format': u'ami',
u'disk_format': u'ami',
@ -2889,11 +2935,12 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
schema = glance.api.v2.images.get_schema(custom_image_properties)
self.serializer = glance.api.v2.images.ResponseSerializer(schema)
props = dict(color='green', mood='grouchy')
self.fixture = _domain_fixture(
UUID2, name='image-2', owner=TENANT2,
checksum='ca425b88f047ce8ec45ee90e813ada91',
created_at=DATETIME, updated_at=DATETIME, size=1024,
extra_properties=dict(color='green', mood='grouchy'))
virtual_size=3072, extra_properties=props)
def test_show(self):
expected = {
@ -2905,6 +2952,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
'tags': [],
'size': 1024,
'virtual_size': 3072,
'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81',
'color': 'green',
'created_at': ISOTIME,
@ -2928,6 +2976,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
'tags': [],
'size': 1024,
'virtual_size': 3072,
'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81',
'color': 'invalid',
'created_at': ISOTIME,
@ -2950,7 +2999,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
UUID2, name='image-2', owner=TENANT2,
checksum='ca425b88f047ce8ec45ee90e813ada91',
created_at=DATETIME, updated_at=DATETIME, size=1024,
extra_properties={'marx': 'groucho'})
virtual_size=3072, extra_properties={'marx': 'groucho'})
def test_show(self):
serializer = glance.api.v2.images.ResponseSerializer()
@ -2964,6 +3013,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
'marx': 'groucho',
'tags': [],
'size': 1024,
'virtual_size': 3072,
'created_at': ISOTIME,
'updated_at': ISOTIME,
'self': '/v2/images/%s' % UUID2,
@ -2991,6 +3041,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
'marx': 123,
'tags': [],
'size': 1024,
'virtual_size': 3072,
'created_at': ISOTIME,
'updated_at': ISOTIME,
'self': '/v2/images/%s' % UUID2,
@ -3014,6 +3065,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
'tags': [],
'size': 1024,
'virtual_size': 3072,
'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81',
'created_at': ISOTIME,
'updated_at': ISOTIME,
@ -3033,8 +3085,8 @@ class TestImagesSerializerDirectUrl(test_utils.BaseTestCase):
self.active_image = _domain_fixture(
UUID1, name='image-1', visibility='public',
status='active', size=1024, created_at=DATETIME,
updated_at=DATETIME,
status='active', size=1024, virtual_size=3072,
created_at=DATETIME, updated_at=DATETIME,
locations=[{'url': 'http://some/fake/location',
'metadata': {}}])

View File

@ -70,10 +70,11 @@ class TestRegistryV2Client(base.IsolatedUnitTest,
self.get_extra_fixture(
id=UUID1, name='fake image #1', is_public=False,
disk_format='ami', container_format='ami', size=13,
virtual_size=26, properties={'type': 'kernel'},
location="swift://user:passwd@acct/container/obj.tar.0",
properties={'type': 'kernel'}, created_at=uuid1_time),
created_at=uuid1_time),
self.get_extra_fixture(id=UUID2, name='fake image #2',
properties={}, size=19,
properties={}, size=19, virtual_size=38,
location="file:///tmp/glance-tests/2",
created_at=uuid2_time)]
self.destroy_fixtures()
@ -204,14 +205,16 @@ class TestRegistryV2Client(base.IsolatedUnitTest,
UUID3 = _gen_uuid()
extra_fixture = self.get_fixture(id=UUID3, name='asdf',
disk_format='ami',
container_format='ami', size=100)
container_format='ami',
size=100, virtual_size=200)
db_api.image_create(self.context, extra_fixture)
UUID4 = _gen_uuid()
extra_fixture = self.get_fixture(id=UUID4, name='asdf',
disk_format='iso',
container_format='bare', size=2)
container_format='bare',
size=2, virtual_size=4)
db_api.image_create(self.context, extra_fixture)
@ -384,7 +387,7 @@ class TestRegistryV2Client(base.IsolatedUnitTest,
def test_image_get(self):
"""Tests that the detailed info about an image returned"""
fixture = self.get_fixture(id=UUID1, name='fake image #1',
is_public=False, size=13,
is_public=False, size=13, virtual_size=26,
disk_format='ami', container_format='ami')
data = self.client.image_get(image_id=UUID1)

View File

@ -32,7 +32,7 @@ class TestSchemasController(test_utils.BaseTestCase):
'disk_format', 'updated_at', 'visibility', 'self',
'file', 'container_format', 'schema', 'id', 'size',
'direct_url', 'min_ram', 'min_disk', 'protected',
'locations', 'owner'])
'locations', 'owner', 'virtual_size'])
self.assertEqual(set(output['properties'].keys()), expected)
def test_images(self):