From 20b7c6975872bfa157f760e6fbe15c135cdd36e6 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 20 Sep 2011 14:24:50 -0400 Subject: [PATCH] Added min_disk and min_ram properties to images Fixes LP Bug#849368 Change-Id: I3e17370537144d117d99af5fa5a21df830b7c7ed --- Authors | 1 + bin/glance | 8 +- doc/source/glanceapi.rst | 26 +++ glance/api/v1/images.py | 5 +- glance/registry/db/api.py | 10 +- .../versions/009_add_mindisk_and_minram.py | 86 +++++++++ glance/registry/db/models.py | 2 + glance/registry/server.py | 2 +- glance/tests/functional/test_bin_glance.py | 35 +++- glance/tests/unit/test_api.py | 174 ++++++++++++++++++ 10 files changed, 335 insertions(+), 14 deletions(-) create mode 100644 glance/registry/db/migrate_repo/versions/009_add_mindisk_and_minram.py diff --git a/Authors b/Authors index 9fe2e0bcda..673e8a37c9 100644 --- a/Authors +++ b/Authors @@ -1,4 +1,5 @@ Adam Gandelman +Alex Meade Andrey Brindeyev Brian Lamar Brian Waldon diff --git a/bin/glance b/bin/glance index f6348787c7..0cbe4a2d54 100755 --- a/bin/glance +++ b/bin/glance @@ -114,7 +114,7 @@ def get_image_filters_from_args(args): return FAILURE SUPPORTED_FILTERS = ['name', 'disk_format', 'container_format', 'status', - 'size_min', 'size_max'] + 'min_ram', 'min_disk', 'size_min', 'size_max'] filters = {} for (key, value) in fields.items(): if key not in SUPPORTED_FILTERS: @@ -141,6 +141,8 @@ def print_image_formatted(client, image): print "Size: %d" % int(image['size']) print "Disk format: %s" % image['disk_format'] print "Container format: %s" % image['container_format'] + print "Minimum Ram Required (MB): %s" % image['min_ram'] + print "Minimum Disk Required (GB): %s" % image['min_disk'] if image['owner']: print "Owner: %s" % image['owner'] if len(image['properties']) > 0: @@ -216,6 +218,8 @@ EXAMPLES 'is_public': common_utils.bool_from_string( fields.pop('is_public', False)), 'disk_format': fields.pop('disk_format', 'raw'), + 'min_disk': fields.pop('min_disk', 0), + 'min_ram': fields.pop('min_ram', 0), 'container_format': fields.pop('container_format', 'ovf')} # Strip any args that are not supported @@ -329,7 +333,7 @@ to spell field names correctly. :)""" fields.pop(field) base_image_fields = ['disk_format', 'container_format', 'name', - 'location', 'owner'] + 'min_disk', 'min_ram', 'location', 'owner'] for field in base_image_fields: fvalue = fields.pop(field, None) if fvalue is not None: diff --git a/doc/source/glanceapi.rst b/doc/source/glanceapi.rst index 11791ead06..290081013b 100644 --- a/doc/source/glanceapi.rst +++ b/doc/source/glanceapi.rst @@ -73,6 +73,8 @@ JSON-encoded mapping in the following format:: 'deleted_at': '', 'status': 'active', 'is_public': true, + 'min_ram': 256, + 'min_disk': 5, 'owner': null, 'properties': {'distro': 'Ubuntu 10.04 LTS'}}, ...]} @@ -95,6 +97,12 @@ JSON-encoded mapping in the following format:: The `is_public` field is a boolean indicating whether the image is publically available + The 'min_ram' field is an integer specifying the minimum amount of + ram needed to run this image on an instance, in megabytes + + The 'min_disk' field is an integer specifying the minimum amount of + disk space needed to run this image on an instance, in gigabytes + The `owner` field is a string which may either be null or which will indicate the owner of the image @@ -181,6 +189,8 @@ following shows an example of the HTTP headers returned from the above x-image-meta-deleted_at x-image-meta-status available x-image-meta-is-public true + x-image-meta-min-ram 256 + x-image-meta-min-disk 0 x-image-meta-owner null x-image-meta-property-distro Ubuntu 10.04 LTS @@ -241,6 +251,8 @@ returned from the above ``GET`` request:: x-image-meta-deleted_at x-image-meta-status available x-image-meta-is-public true + x-image-meta-min-ram 256 + x-image-meta-min-disk 5 x-image-meta-owner null x-image-meta-property-distro Ubuntu 10.04 LTS @@ -383,6 +395,20 @@ The list of metadata headers that Glance accepts are listed below. When not present, the image is assumed to be *not public* and specific to a user. +* ``x-image-meta-min-ram`` + + This header is optional. When present it shall be the expected minimum ram + required in megabytes to run this image on a server. + + When not present, the image is assumed to have a minimum ram requirement of 0. + +* ``x-image-meta-min-disk`` + + This header is optional. When present it shall be the expected minimum disk + space required in gigabytes to run this image on a server. + + When not present, the image is assumed to have a minimum disk space requirement of 0. + * ``x-image-meta-owner`` This header is optional and only meaningful for admins. diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py index b5e4b3183d..b80a164354 100644 --- a/glance/api/v1/images.py +++ b/glance/api/v1/images.py @@ -55,7 +55,8 @@ from glance import utils logger = logging.getLogger('glance.api.v1.images') SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format', - 'size_min', 'size_max', 'is_public'] + 'min_ram', 'min_disk', 'size_min', 'size_max', + 'is_public'] SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir') @@ -130,6 +131,8 @@ class Controller(api.BaseController): 'disk_format': , 'container_format': , 'checksum': , + 'min_disk': , + 'min_ram': , 'store': , 'status': , 'created_at': , diff --git a/glance/registry/db/api.py b/glance/registry/db/api.py index a1f2fa4680..cb53f7614d 100644 --- a/glance/registry/db/api.py +++ b/glance/registry/db/api.py @@ -47,8 +47,8 @@ BASE_MODEL_ATTRS = set(['id', 'created_at', 'updated_at', 'deleted_at', IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size', 'disk_format', 'container_format', - 'is_public', 'location', 'checksum', - 'owner']) + 'min_disk', 'min_ram', 'is_public', + 'location', 'checksum', 'owner']) CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf'] DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', @@ -327,6 +327,12 @@ def _image_update(context, values, image_id, purge_props=False): if 'size' in values: values['size'] = int(values['size']) + if 'min_ram' in values: + values['min_ram'] = int(values['min_ram']) + + if 'min_disk' in values: + values['min_disk'] = int(values['min_disk']) + values['is_public'] = bool(values.get('is_public', False)) image_ref = models.Image() diff --git a/glance/registry/db/migrate_repo/versions/009_add_mindisk_and_minram.py b/glance/registry/db/migrate_repo/versions/009_add_mindisk_and_minram.py new file mode 100644 index 0000000000..2b4cc7d9bc --- /dev/null +++ b/glance/registry/db/migrate_repo/versions/009_add_mindisk_and_minram.py @@ -0,0 +1,86 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from migrate.changeset import * +from sqlalchemy import * +from sqlalchemy.sql import and_, not_ + +from glance.registry.db.migrate_repo.schema import ( + Boolean, DateTime, Integer, String, Text, from_migration_import) + + +def get_images_table(meta): + """ + Returns the Table object for the images table that + corresponds to the images table definition of this version. + """ + images = Table('images', meta, + Column('id', Integer(), primary_key=True, nullable=False), + Column('name', String(255)), + Column('disk_format', String(20)), + Column('container_format', String(20)), + Column('size', Integer()), + Column('status', String(30), nullable=False), + Column('is_public', Boolean(), nullable=False, default=False, + index=True), + Column('location', Text()), + Column('created_at', DateTime(), nullable=False), + Column('updated_at', DateTime()), + Column('deleted_at', DateTime()), + Column('deleted', Boolean(), nullable=False, default=False, + index=True), + Column('checksum', String(32)), + Column('min_disk', Integer(), default=0), + Column('min_ram', Integer(), default=0), + mysql_engine='InnoDB', + useexisting=True) + + return images + + +def get_image_properties_table(meta): + """ + No changes to the image properties table from 008... + """ + (define_image_properties_table,) = from_migration_import( + '008_add_image_members_table', ['define_image_properties_table']) + + image_properties = define_image_properties_table(meta) + return image_properties + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + images = get_images_table(meta) + + min_disk = Column('min_disk', Integer(), default=0) + min_disk.create(images) + + min_ram = Column('min_ram', Integer(), default=0) + min_ram.create(images) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + images = get_images_table(meta) + + images.columns['min_disk'].drop() + images.columns['min_ram'].drop() diff --git a/glance/registry/db/models.py b/glance/registry/db/models.py index 9c5ce1e22b..a66bc67c1e 100644 --- a/glance/registry/db/models.py +++ b/glance/registry/db/models.py @@ -105,6 +105,8 @@ class Image(BASE, ModelBase): is_public = Column(Boolean, nullable=False, default=False) location = Column(Text) checksum = Column(String(32)) + min_disk = Column(Integer(), default=0) + min_ram = Column(Integer(), default=0) owner = Column(String(255)) diff --git a/glance/registry/server.py b/glance/registry/server.py index 5c3780e8e3..8eebc2be85 100644 --- a/glance/registry/server.py +++ b/glance/registry/server.py @@ -38,7 +38,7 @@ DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size', 'checksum'] SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format', - 'size_min', 'size_max'] + 'min_ram', 'min_disk', 'size_min', 'size_max'] SUPPORTED_SORT_KEYS = ('name', 'status', 'container_format', 'disk_format', 'size', 'id', 'created_at', 'updated_at') diff --git a/glance/tests/functional/test_bin_glance.py b/glance/tests/functional/test_bin_glance.py index e8be3a32ef..bf441daff6 100644 --- a/glance/tests/functional/test_bin_glance.py +++ b/glance/tests/functional/test_bin_glance.py @@ -318,7 +318,8 @@ class TestBinGlance(functional.FunctionalTest): _add_args = [ "name=Name1 disk_format=vhd container_format=ovf foo=bar", "name=Name2 disk_format=ami container_format=ami foo=bar", - "name=Name3 disk_format=vhd container_format=ovf foo=baz", + "name=Name3 disk_format=vhd container_format=ovf foo=baz " + "min_disk=7 min_ram=256", ] for i, args in enumerate(_add_args): @@ -402,9 +403,27 @@ class TestBinGlance(functional.FunctionalTest): self.assertEqual(0, exitcode) image_lines = out.split("\n")[1:-1] - self.assertEqual(20, len(image_lines)) + self.assertEqual(24, len(image_lines)) self.assertTrue(image_lines[1].startswith('Id: 2')) - self.assertTrue(image_lines[11].startswith('Id: 1')) + self.assertTrue(image_lines[13].startswith('Id: 1')) + + # 10. Check min_ram filter + cmd = "min_ram=256" + exitcode, out, err = execute("%s %s" % (_details_cmd, cmd)) + + self.assertEqual(0, exitcode) + image_lines = out.split("\n")[2:-1] + self.assertEqual(11, len(image_lines)) + self.assertTrue(image_lines[0].startswith('Id: 3')) + + # 11. Check min_disk filter + cmd = "min_disk=7" + exitcode, out, err = execute("%s %s" % (_details_cmd, cmd)) + + self.assertEqual(0, exitcode) + image_lines = out.split("\n")[2:-1] + self.assertEqual(11, len(image_lines)) + self.assertTrue(image_lines[0].startswith('Id: 3')) self.stop_servers() @@ -483,9 +502,9 @@ class TestBinGlance(functional.FunctionalTest): self.assertEqual(0, exitcode) image_lines = out.split("\n")[1:-1] - self.assertEqual(18, len(image_lines)) + self.assertEqual(22, len(image_lines)) self.assertTrue(image_lines[1].startswith('Id: 3')) - self.assertTrue(image_lines[10].startswith('Id: 1')) + self.assertTrue(image_lines[12].startswith('Id: 1')) def test_results_sorting(self): self.cleanup() @@ -555,7 +574,7 @@ class TestBinGlance(functional.FunctionalTest): self.assertEqual(0, exitcode) image_lines = out.split("\n")[1:-1] - self.assertEqual(27, len(image_lines)) + self.assertEqual(33, len(image_lines)) self.assertTrue(image_lines[1].startswith('Id: 3')) - self.assertTrue(image_lines[10].startswith('Id: 2')) - self.assertTrue(image_lines[19].startswith('Id: 5')) + self.assertTrue(image_lines[12].startswith('Id: 2')) + self.assertTrue(image_lines[23].startswith('Id: 5')) diff --git a/glance/tests/unit/test_api.py b/glance/tests/unit/test_api.py index e7a619bda6..756f07dbbf 100644 --- a/glance/tests/unit/test_api.py +++ b/glance/tests/unit/test_api.py @@ -62,6 +62,8 @@ class TestRegistryAPI(unittest.TestCase): 'deleted_at': None, 'deleted': False, 'checksum': None, + 'min_disk': 0, + 'min_ram': 0, 'size': 13, 'location': "swift://user:passwd@acct/container/obj.tar.0", 'properties': {'type': 'kernel'}}, @@ -76,6 +78,8 @@ class TestRegistryAPI(unittest.TestCase): 'deleted_at': None, 'deleted': False, 'checksum': None, + 'min_disk': 5, + 'min_ram': 256, 'size': 19, 'location': "file:///tmp/glance-tests/2", 'properties': {}}] @@ -107,6 +111,8 @@ class TestRegistryAPI(unittest.TestCase): fixture = {'id': 2, 'name': 'fake image #2', 'size': 19, + 'min_ram': 256, + 'min_disk': 5, 'checksum': None} req = webob.Request.blank('/images/2') res = req.get_response(self.api) @@ -779,6 +785,8 @@ class TestRegistryAPI(unittest.TestCase): 'name': 'fake image #2', 'is_public': True, 'size': 19, + 'min_disk': 5, + 'min_ram': 256, 'checksum': None, 'disk_format': 'vhd', 'container_format': 'ovf', @@ -958,6 +966,84 @@ class TestRegistryAPI(unittest.TestCase): for image in images: self.assertEqual('ovf', image['container_format']) + def test_get_details_filter_min_disk(self): + """ + Tests that the /images/detail registry API returns list of + public images that have a specific min_disk + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'fake image #3', + 'size': 19, + 'min_disk': 7, + 'checksum': None} + + db_api.image_create(self.context, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': 'fake image #4', + 'size': 19, + 'checksum': None} + + db_api.image_create(self.context, extra_fixture) + + req = webob.Request.blank('/images/detail?min_disk=7') + res = req.get_response(self.api) + res_dict = json.loads(res.body) + self.assertEquals(res.status_int, 200) + + images = res_dict['images'] + self.assertEquals(len(images), 1) + + for image in images: + self.assertEqual(7, image['min_disk']) + + def test_get_details_filter_min_ram(self): + """ + Tests that the /images/detail registry API returns list of + public images that have a specific min_ram + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'fake image #3', + 'size': 19, + 'min_ram': 514, + 'checksum': None} + + db_api.image_create(self.context, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': 'fake image #4', + 'size': 19, + 'checksum': None} + + db_api.image_create(self.context, extra_fixture) + + req = webob.Request.blank('/images/detail?min_ram=514') + res = req.get_response(self.api) + res_dict = json.loads(res.body) + self.assertEquals(res.status_int, 200) + + images = res_dict['images'] + self.assertEquals(len(images), 1) + + for image in images: + self.assertEqual(514, image['min_ram']) + def test_get_details_filter_disk_format(self): """ Tests that the /images/detail registry API returns list of @@ -1307,6 +1393,92 @@ class TestRegistryAPI(unittest.TestCase): # Test status was updated properly self.assertEquals('active', res_dict['image']['status']) + def test_create_image_with_min_disk(self): + """Tests that the /images POST registry API creates the image""" + fixture = {'name': 'fake public image', + 'is_public': True, + 'min_disk': 5, + 'disk_format': 'vhd', + 'container_format': 'ovf'} + + req = webob.Request.blank('/images') + + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(dict(image=fixture)) + + res = req.get_response(self.api) + + self.assertEquals(res.status_int, 200) + + res_dict = json.loads(res.body) + + self.assertEquals(5, res_dict['image']['min_disk']) + + def test_create_image_with_min_ram(self): + """Tests that the /images POST registry API creates the image""" + fixture = {'name': 'fake public image', + 'is_public': True, + 'min_ram': 256, + 'disk_format': 'vhd', + 'container_format': 'ovf'} + + req = webob.Request.blank('/images') + + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(dict(image=fixture)) + + res = req.get_response(self.api) + + self.assertEquals(res.status_int, 200) + + res_dict = json.loads(res.body) + + self.assertEquals(256, res_dict['image']['min_ram']) + + def test_create_image_with_min_ram_default(self): + """Tests that the /images POST registry API creates the image""" + fixture = {'name': 'fake public image', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf'} + + req = webob.Request.blank('/images') + + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(dict(image=fixture)) + + res = req.get_response(self.api) + + self.assertEquals(res.status_int, 200) + + res_dict = json.loads(res.body) + + self.assertEquals(0, res_dict['image']['min_ram']) + + def test_create_image_with_min_disk_default(self): + """Tests that the /images POST registry API creates the image""" + fixture = {'name': 'fake public image', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf'} + + req = webob.Request.blank('/images') + + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(dict(image=fixture)) + + res = req.get_response(self.api) + + self.assertEquals(res.status_int, 200) + + res_dict = json.loads(res.body) + + self.assertEquals(0, res_dict['image']['min_disk']) + def test_create_image_with_bad_container_format(self): """Tests proper exception is raised if a bad disk_format is set""" fixture = {'id': 3, @@ -1385,6 +1557,8 @@ class TestRegistryAPI(unittest.TestCase): def test_update_image(self): """Tests that the /images PUT registry API updates the image""" fixture = {'name': 'fake public image #2', + 'min_disk': 5, + 'min_ram': 256, 'disk_format': 'raw'} req = webob.Request.blank('/images/2')