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'])