From 64736d1e12b9754562d2980234a96f6eeb18e704 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 15 Sep 2011 14:46:19 -0400 Subject: [PATCH] Add minDisk and minRam to OSAPI image details Change-Id: I4bf1920a245de85c88c38ec3ad82dc0e93cc671c --- nova/api/openstack/create_instance_helper.py | 4 + nova/api/openstack/images.py | 6 + nova/api/openstack/schemas/v1.1/image.rng | 6 + nova/api/openstack/views/images.py | 5 + nova/compute/api.py | 5 + nova/exception.py | 8 + nova/image/glance.py | 2 +- nova/tests/api/openstack/test_images.py | 189 +++++++++++++++++++ nova/tests/image/test_glance.py | 12 ++ nova/tests/test_compute.py | 91 +++++++++ 10 files changed, 327 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 79f17e27f17e..dde9187cd657 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -187,6 +187,10 @@ class CreateInstanceHelper(object): config_drive=config_drive,)) except quota.QuotaError as error: self._handle_quota_error(error) + except exception.InstanceTypeMemoryTooSmall as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) + except exception.InstanceTypeDiskTooSmall as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) except exception.ImageNotFound as error: msg = _("Can not find requested image") raise exc.HTTPBadRequest(explanation=msg) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index d579ae716084..7795a3fc4b7d 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -41,6 +41,8 @@ SUPPORTED_FILTERS = { 'changes-since': 'changes-since', 'server': 'property-instance_ref', 'type': 'property-image_type', + 'minRam': 'min_ram', + 'minDisk': 'min_disk', } @@ -239,6 +241,10 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): image_elem.set('status', str(image_dict['status'])) if 'progress' in image_dict: image_elem.set('progress', str(image_dict['progress'])) + if 'minRam' in image_dict: + image_elem.set('minRam', str(image_dict['minRam'])) + if 'minDisk' in image_dict: + image_elem.set('minDisk', str(image_dict['minDisk'])) if 'server' in image_dict: server_elem = self._create_server_node(image_dict['server']) image_elem.append(server_elem) diff --git a/nova/api/openstack/schemas/v1.1/image.rng b/nova/api/openstack/schemas/v1.1/image.rng index 887f767512df..505081fbabac 100644 --- a/nova/api/openstack/schemas/v1.1/image.rng +++ b/nova/api/openstack/schemas/v1.1/image.rng @@ -8,6 +8,12 @@ + + + + + + diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 659bfd46328b..e366661c331e 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -161,6 +161,11 @@ class ViewBuilderV11(ViewBuilder): if detail: image["metadata"] = image_obj.get("properties", {}) + if 'min_ram' in image_obj: + image["minRam"] = image_obj.get("min_ram") or 0 + + if 'min_disk' in image_obj: + image["minDisk"] = image_obj.get("min_disk") or 0 return image diff --git a/nova/compute/api.py b/nova/compute/api.py index cedbd747ab0e..54c847a0ac07 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -219,6 +219,11 @@ class API(base.Base): image_href) image = image_service.show(context, image_id) + if instance_type['memory_mb'] < int(image.get('min_ram', 0)): + raise exception.InstanceTypeMemoryTooSmall() + if instance_type['local_gb'] < int(image.get('min_disk', 0)): + raise exception.InstanceTypeDiskTooSmall() + config_drive_id = None if config_drive and config_drive is not True: # config_drive is volume id diff --git a/nova/exception.py b/nova/exception.py index ea10a54efc07..6a50faa4ace0 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -810,3 +810,11 @@ class ZoneRequestError(Error): if message is None: message = _("1 or more Zones could not complete the request") super(ZoneRequestError, self).__init__(message=message) + + +class InstanceTypeMemoryTooSmall(NovaException): + message = _("Instance type's memory is too small for requested image.") + + +class InstanceTypeDiskTooSmall(NovaException): + message = _("Instance type's disk is too small for requested image.") diff --git a/nova/image/glance.py b/nova/image/glance.py index 5ee1d2b8a988..9a915d02d282 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -404,7 +404,7 @@ def _limit_attributes(image_meta): 'container_format', 'checksum', 'id', 'name', 'created_at', 'updated_at', 'deleted_at', 'deleted', 'status', - 'is_public'] + 'min_disk', 'min_ram', 'is_public'] output = {} for attr in IMAGE_ATTRIBUTES: output[attr] = image_meta.get(attr) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 886efb6acbef..c935003e2a22 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -113,6 +113,7 @@ class ImagesTest(test.TestCase): self.assertDictMatch(expected_image, actual_image) def test_get_image_v1_1(self): + self.maxDiff = None request = webob.Request.blank('/v1.1/fake/images/124') app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) response = request.get_response(app) @@ -133,6 +134,8 @@ class ImagesTest(test.TestCase): "created": NOW_API_FORMAT, "status": "SAVING", "progress": 0, + "minDisk": 0, + "minRam": 0, 'server': { 'id': '42', "links": [{ @@ -542,6 +545,8 @@ class ImagesTest(test.TestCase): 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, + 'minDisk': 0, + 'minRam': 0, "links": [{ "rel": "self", "href": "http://localhost/v1.1/fake/images/123", @@ -567,6 +572,8 @@ class ImagesTest(test.TestCase): 'created': NOW_API_FORMAT, 'status': 'SAVING', 'progress': 0, + 'minDisk': 0, + 'minRam': 0, 'server': { 'id': '42', "links": [{ @@ -603,6 +610,8 @@ class ImagesTest(test.TestCase): 'created': NOW_API_FORMAT, 'status': 'SAVING', 'progress': 0, + 'minDisk': 0, + 'minRam': 0, 'server': { 'id': '42', "links": [{ @@ -639,6 +648,8 @@ class ImagesTest(test.TestCase): 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, + 'minDisk': 0, + 'minRam': 0, 'server': { 'id': '42', "links": [{ @@ -675,6 +686,8 @@ class ImagesTest(test.TestCase): 'created': NOW_API_FORMAT, 'status': 'ERROR', 'progress': 0, + 'minDisk': 0, + 'minRam': 0, 'server': { 'id': '42', "links": [{ @@ -711,6 +724,8 @@ class ImagesTest(test.TestCase): 'created': NOW_API_FORMAT, 'status': 'DELETED', 'progress': 0, + 'minDisk': 0, + 'minRam': 0, 'server': { 'id': '42', "links": [{ @@ -747,6 +762,8 @@ class ImagesTest(test.TestCase): 'created': NOW_API_FORMAT, 'status': 'DELETED', 'progress': 0, + 'minDisk': 0, + 'minRam': 0, 'server': { 'id': '42', "links": [{ @@ -780,6 +797,8 @@ class ImagesTest(test.TestCase): 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, + 'minDisk': 0, + 'minRam': 0, "links": [{ "rel": "self", "href": "http://localhost/v1.1/fake/images/130", @@ -810,6 +829,30 @@ class ImagesTest(test.TestCase): controller.index(request) self.mox.VerifyAll() + def test_image_filter_with_min_ram(self): + image_service = self.mox.CreateMockAnything() + context = self._get_fake_context() + filters = {'min_ram': '0'} + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images?minRam=0') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_with_min_disk(self): + image_service = self.mox.CreateMockAnything() + context = self._get_fake_context() + filters = {'min_disk': '7'} + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images?minDisk=7') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + def test_image_filter_with_status(self): image_service = self.mox.CreateMockAnything() context = self._get_fake_context() @@ -1369,6 +1412,152 @@ class ImageXMLSerializationTest(test.TestCase): server_root = root.find('{0}server'.format(NS)) self.assertEqual(server_root, None) + def test_show_with_min_ram(self): + serializer = images.ImageXMLSerializer() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'minRam': 256, + 'server': { + 'id': '1', + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture, 'show') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status', 'progress', + 'minRam']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_show_with_min_disk(self): + serializer = images.ImageXMLSerializer() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'minDisk': 5, + 'server': { + 'id': '1', + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture, 'show') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status', 'progress', + 'minDisk']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + def test_index(self): serializer = images.ImageXMLSerializer() diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py index 290c9a04a6eb..c592b488865b 100644 --- a/nova/tests/image/test_glance.py +++ b/nova/tests/image/test_glance.py @@ -127,6 +127,8 @@ class TestGlanceImageService(test.TestCase): 'name': 'test image', 'is_public': False, 'size': None, + 'min_disk': None, + 'min_ram': None, 'location': None, 'disk_format': None, 'container_format': None, @@ -157,6 +159,8 @@ class TestGlanceImageService(test.TestCase): 'name': 'test image', 'is_public': False, 'size': None, + 'min_disk': None, + 'min_ram': None, 'location': None, 'disk_format': None, 'container_format': None, @@ -287,6 +291,8 @@ class TestGlanceImageService(test.TestCase): 'name': 'TestImage %d' % (i), 'properties': {}, 'size': None, + 'min_disk': None, + 'min_ram': None, 'location': None, 'disk_format': None, 'container_format': None, @@ -330,6 +336,8 @@ class TestGlanceImageService(test.TestCase): 'name': 'TestImage %d' % (i), 'properties': {}, 'size': None, + 'min_disk': None, + 'min_ram': None, 'location': None, 'disk_format': None, 'container_format': None, @@ -382,6 +390,8 @@ class TestGlanceImageService(test.TestCase): 'name': 'image1', 'is_public': True, 'size': None, + 'min_disk': None, + 'min_ram': None, 'location': None, 'disk_format': None, 'container_format': None, @@ -416,6 +426,8 @@ class TestGlanceImageService(test.TestCase): 'name': 'image10', 'is_public': True, 'size': None, + 'min_disk': None, + 'min_ram': None, 'location': None, 'disk_format': None, 'container_format': None, diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 7ce9e740a29e..2af0a6012f25 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -20,6 +20,8 @@ Tests For Compute """ +from copy import copy + from nova import compute from nova import context from nova import db @@ -1394,3 +1396,92 @@ class ComputeTestCase(test.TestCase): self.assertEqual(self.compute_api._volume_size(inst_type, 'swap'), swap_size) + + +class ComputeTestMinRamMinDisk(test.TestCase): + def setUp(self): + super(ComputeTestMinRamMinDisk, self).setUp() + self.compute = utils.import_object(FLAGS.compute_manager) + self.compute_api = compute.API() + self.context = context.RequestContext('fake', 'fake') + self.fake_image = { + 'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}} + + def test_create_with_too_little_ram(self): + """Test an instance type with too little memory""" + + inst_type = instance_types.get_default_instance_type() + inst_type['memory_mb'] = 1 + + def fake_show(*args): + img = copy(self.fake_image) + img['min_ram'] = 2 + return img + self.stubs.Set(fake_image._FakeImageService, 'show', fake_show) + + self.assertRaises(exception.InstanceTypeMemoryTooSmall, + self.compute_api.create, self.context, inst_type, None) + + # Now increase the inst_type memory and make sure all is fine. + inst_type['memory_mb'] = 2 + ref = self.compute_api.create(self.context, inst_type, None) + self.assertTrue(ref) + + db.instance_destroy(self.context, ref[0]['id']) + + def test_create_with_too_little_disk(self): + """Test an instance type with too little disk space""" + + inst_type = instance_types.get_default_instance_type() + inst_type['local_gb'] = 1 + + def fake_show(*args): + img = copy(self.fake_image) + img['min_disk'] = 2 + return img + self.stubs.Set(fake_image._FakeImageService, 'show', fake_show) + + self.assertRaises(exception.InstanceTypeDiskTooSmall, + self.compute_api.create, self.context, inst_type, None) + + # Now increase the inst_type disk space and make sure all is fine. + inst_type['local_gb'] = 2 + ref = self.compute_api.create(self.context, inst_type, None) + self.assertTrue(ref) + + db.instance_destroy(self.context, ref[0]['id']) + + def test_create_just_enough_ram_and_disk(self): + """Test an instance type with just enough ram and disk space""" + + inst_type = instance_types.get_default_instance_type() + inst_type['local_gb'] = 2 + inst_type['memory_mb'] = 2 + + def fake_show(*args): + img = copy(self.fake_image) + img['min_ram'] = 2 + img['min_disk'] = 2 + return img + self.stubs.Set(fake_image._FakeImageService, 'show', fake_show) + + ref = self.compute_api.create(self.context, inst_type, None) + self.assertTrue(ref) + + db.instance_destroy(self.context, ref[0]['id']) + + def test_create_with_no_ram_and_disk_reqs(self): + """Test an instance type with no min_ram or min_disk""" + + inst_type = instance_types.get_default_instance_type() + inst_type['local_gb'] = 1 + inst_type['memory_mb'] = 1 + + def fake_show(*args): + return copy(self.fake_image) + self.stubs.Set(fake_image._FakeImageService, 'show', fake_show) + + ref = self.compute_api.create(self.context, inst_type, None) + self.assertTrue(ref) + + db.instance_destroy(self.context, ref[0]['id'])