Skip to content

Instantly share code, notes, and snippets.

@mgagne
Created August 26, 2016 18:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mgagne/3d6d7596537ac6da4f9195a54df12909 to your computer and use it in GitHub Desktop.
Save mgagne/3d6d7596537ac6da4f9195a54df12909 to your computer and use it in GitHub Desktop.
From: =?UTF-8?q?Mathieu=20Gagn=C3=A9?= <mgagne@iweb.com>
Date: Mon, 11 Apr 2016 22:09:41 -0400
Subject: Add ability to restrict image to specific flavor classes
---
nova/compute/api.py | 28 ++++++++++
nova/tests/unit/compute/test_compute.py | 88 +++++++++++++++++++++++++++++++
2 files changed, 116 insertions(+)
diff --git a/nova/compute/api.py b/nova/compute/api.py
index f48d027..2598396 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -652,6 +652,26 @@ class API(base.Base):
# reason, we rely on the DB to cast True to a String.
return True if bool_val else ''
+ def _validate_image_flavor_classes(self, image_flavor_classes,
+ instance_type_class):
+ # Following cases are supported:
+ # - *: All flavor classes
+ # - *,!virtual: All flavor classes but "virtual"
+ # - virtual: "virtual" flavor classes only
+ flavor_classes = image_flavor_classes.split(',')
+ passed = (flavor_classes[0] == '*')
+ for flavor_class in flavor_classes:
+ negative = False
+ if flavor_class.startswith('!'):
+ negative = True
+ flavor_class = flavor_class[1:]
+ if instance_type_class == flavor_class:
+ # Negative operator are absolute
+ if negative:
+ return False
+ passed |= True
+ return passed
+
def _check_requested_image(self, context, image_id, image,
instance_type, root_bdm):
if not image:
@@ -660,7 +680,15 @@ class API(base.Base):
if image['status'] != 'active':
raise exception.ImageNotActive(image_id=image_id)
+ # NOTE(mgagne) Restrict image usage to flavor classes (if applicable)
image_properties = image.get('properties', {})
+ image_flavor_classes = image_properties.get('flavor_classes', '*')
+ instance_type_class = instance_type.get('extra_specs', {}).get('class')
+ if not self._validate_image_flavor_classes(image_flavor_classes,
+ instance_type_class):
+ msg = "Image cannot be used with provided flavor."
+ raise exception.InvalidRequest(msg)
+
config_drive_option = image_properties.get(
'img_config_drive', 'optional')
if config_drive_option not in ['optional', 'mandatory']:
diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py
index 3549338..2a4d163 100644
--- a/nova/tests/unit/compute/test_compute.py
+++ b/nova/tests/unit/compute/test_compute.py
@@ -7354,6 +7354,94 @@ class ComputeAPITestCase(BaseTestCase):
self.assertIsNone(instance['task_state'])
return instance, instance_uuid
+ def test_create_with_image_flavor_classes_any(self):
+ # Test an image with no flavor class restrictions.
+
+ inst_type = flavors.get_default_flavor()
+ inst_type['extra_specs'] = {'class': 'virtual'}
+
+ self.fake_image['properties'] = {'flavor_classes': '*'}
+ self.stubs.Set(fake_image._FakeImageService, 'show', self.fake_show)
+
+ (refs, resv_id) = self.compute_api.create(self.context,
+ inst_type, self.fake_image['id'])
+
+ def test_create_with_image_flavor_classes_only_one(self):
+ # Test an image with no flavor class restrictions.
+
+ inst_type = flavors.get_default_flavor()
+ inst_type['extra_specs'] = {'class': 'virtual'}
+
+ self.fake_image['properties'] = {'flavor_classes': 'metal'}
+ self.stubs.Set(fake_image._FakeImageService, 'show', self.fake_show)
+
+ # Negative test
+ self.assertRaises(exception.InvalidRequest,
+ self.compute_api.create, self.context,
+ inst_type, self.fake_image['id'])
+
+ # Positive test
+ inst_type['extra_specs'] = {'class': 'metal'}
+ (refs, resv_id) = self.compute_api.create(self.context,
+ inst_type, self.fake_image['id'])
+
+ def test_create_with_image_flavor_classes_only_two(self):
+ # Test an image with no flavor class restrictions.
+
+ inst_type = flavors.get_default_flavor()
+ inst_type['extra_specs'] = {'class': 'container'}
+
+ self.fake_image['properties'] = {'flavor_classes': 'virtual,metal'}
+ self.stubs.Set(fake_image._FakeImageService, 'show', self.fake_show)
+
+ # Negative test
+ self.assertRaises(exception.InvalidRequest,
+ self.compute_api.create, self.context,
+ inst_type, self.fake_image['id'])
+
+ # Positive test
+ inst_type['extra_specs'] = {'class': 'metal'}
+ (refs, resv_id) = self.compute_api.create(self.context,
+ inst_type, self.fake_image['id'])
+
+ def test_create_with_image_flavor_classes_all_but_one(self):
+ # Test an image with no flavor class restrictions.
+
+ inst_type = flavors.get_default_flavor()
+ inst_type['extra_specs'] = {'class': 'virtual'}
+
+ self.fake_image['properties'] = {'flavor_classes': '*,!virtual'}
+ self.stubs.Set(fake_image._FakeImageService, 'show', self.fake_show)
+
+ # Negative test
+ self.assertRaises(exception.InvalidRequest,
+ self.compute_api.create, self.context,
+ inst_type, self.fake_image['id'])
+
+ # Positive test
+ inst_type['extra_specs'] = {'class': 'metal'}
+ (refs, resv_id) = self.compute_api.create(self.context,
+ inst_type, self.fake_image['id'])
+
+ def test_create_with_image_flavor_classes_all_but_two(self):
+ # Test an image with no flavor class restrictions.
+
+ inst_type = flavors.get_default_flavor()
+ inst_type['extra_specs'] = {'class': 'metal'}
+
+ self.fake_image['properties'] = {'flavor_classes': '*,!virtual,!metal'}
+ self.stubs.Set(fake_image._FakeImageService, 'show', self.fake_show)
+
+ # Negative test
+ self.assertRaises(exception.InvalidRequest,
+ self.compute_api.create, self.context,
+ inst_type, self.fake_image['id'])
+
+ # Positive test
+ inst_type['extra_specs'] = {'class': 'container'}
+ (refs, resv_id) = self.compute_api.create(self.context,
+ inst_type, self.fake_image['id'])
+
def test_create_with_too_little_ram(self):
# Test an instance type with too little memory.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment