diff --git a/glance/api/authorization.py b/glance/api/authorization.py index 026f8b861f..34d3a446cf 100644 --- a/glance/api/authorization.py +++ b/glance/api/authorization.py @@ -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) diff --git a/glance/api/v2/images.py b/glance/api/v2/images.py index 3e8d5108dc..279e3d369f 100644 --- a/glance/api/v2/images.py +++ b/glance/api/v2/images.py @@ -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'), diff --git a/glance/db/__init__.py b/glance/db/__init__.py index 6ea8cfac2b..037ce3e7c1 100644 --- a/glance/db/__init__.py +++ b/glance/db/__init__.py @@ -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), } diff --git a/glance/domain/__init__.py b/glance/domain/__init__.py index e4393bd506..d9e4c34f9c 100644 --- a/glance/domain/__init__.py +++ b/glance/domain/__init__.py @@ -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 diff --git a/glance/domain/proxy.py b/glance/domain/proxy.py index 63234f0bf6..157a31fa69 100644 --- a/glance/domain/proxy.py +++ b/glance/domain/proxy.py @@ -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') diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py index 1c4effaf69..d49cdab8cb 100644 --- a/glance/tests/functional/v2/test_images.py +++ b/glance/tests/functional/v2/test_images.py @@ -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: diff --git a/glance/tests/functional/v2/test_schemas.py b/glance/tests/functional/v2/test_schemas.py index 00cff511d2..c1ea343892 100644 --- a/glance/tests/functional/v2/test_schemas.py +++ b/glance/tests/functional/v2/test_schemas.py @@ -41,6 +41,7 @@ class TestSchemas(functional.FunctionalTest): 'updated_at', 'tags', 'size', + 'virtual_size', 'owner', 'container_format', 'disk_format', diff --git a/glance/tests/unit/test_cache_middleware.py b/glance/tests/unit/test_cache_middleware.py index 980976d234..8c2df2f27d 100644 --- a/glance/tests/unit/test_cache_middleware.py +++ b/glance/tests/unit/test_cache_middleware.py @@ -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': '', diff --git a/glance/tests/unit/v2/test_image_data_resource.py b/glance/tests/unit/v2/test_image_data_resource.py index 09716f2aaf..3b8804f27b 100644 --- a/glance/tests/unit/v2/test_image_data_resource.py +++ b/glance/tests/unit/v2/test_image_data_resource.py @@ -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'] diff --git a/glance/tests/unit/v2/test_images_resource.py b/glance/tests/unit/v2/test_images_resource.py index 04c3d40c77..9081580507 100644 --- a/glance/tests/unit/v2/test_images_resource.py +++ b/glance/tests/unit/v2/test_images_resource.py @@ -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': {}}]) diff --git a/glance/tests/unit/v2/test_registry_client.py b/glance/tests/unit/v2/test_registry_client.py index eb1f3e8593..dd0657be89 100644 --- a/glance/tests/unit/v2/test_registry_client.py +++ b/glance/tests/unit/v2/test_registry_client.py @@ -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) diff --git a/glance/tests/unit/v2/test_schemas_resource.py b/glance/tests/unit/v2/test_schemas_resource.py index 8110f21e6d..3d3259e642 100644 --- a/glance/tests/unit/v2/test_schemas_resource.py +++ b/glance/tests/unit/v2/test_schemas_resource.py @@ -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):