summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
authorPratik Mallya <pratik.mallya@gmail.com>2015-12-16 17:34:56 -0600
committerJason Dunsmore <jasondunsmore@gmail.com>2016-02-22 12:51:13 -0600
commit006db59851a43838b4f05b43d654f4dd8da7c188 (patch)
treed2de187577ffc177627046d5d52265c94d2c78bd /contrib
parentdc1bec455a22ef2c30d3b94c22f21e30193c370e (diff)
downloadheat-006db59851a43838b4f05b43d654f4dd8da7c188.tar.gz
Add image/flavor validation to Rackspace Server
The image and flavor used to build a Rackspace server must satisfy certain properties to successfully build the server. This patch adds Rackspace Public Cloud specific validation to enable such checking so that invalid combinations are discovered before the start of stack creation. Change-Id: I2f676b5c06190ddc1077c13bbe3482a23d1d01fd Co-Authored-By: Anna Eilering <anna.eilering@rackspace.com>, Jason Dunsmore <jasondunsmore@gmail.com>
Diffstat (limited to 'contrib')
-rw-r--r--contrib/rackspace/rackspace/resources/cloud_server.py67
-rw-r--r--contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py132
2 files changed, 199 insertions, 0 deletions
diff --git a/contrib/rackspace/rackspace/resources/cloud_server.py b/contrib/rackspace/rackspace/resources/cloud_server.py
index eb70d21a9..c59f6468c 100644
--- a/contrib/rackspace/rackspace/resources/cloud_server.py
+++ b/contrib/rackspace/rackspace/resources/cloud_server.py
@@ -55,6 +55,26 @@ class CloudServer(server.Server):
RC_STATUS_FAILED = 'FAILED'
RC_STATUS_UNPROCESSABLE = 'UNPROCESSABLE'
+ # Nova Extra specs
+ FLAVOR_EXTRA_SPECS = 'OS-FLV-WITH-EXT-SPECS:extra_specs'
+ FLAVOR_CLASSES_KEY = 'flavor_classes'
+ FLAVOR_ACCEPT_ANY = '*'
+ FLAVOR_CLASS = 'class'
+ DISK_IO_INDEX = 'disk_io_index'
+ FLAVOR_CLASSES = (
+ GENERAL1, MEMORY1, PERFORMANCE2, PERFORMANCE1, STANDARD1, IO1,
+ ONMETAL, COMPUTE1
+ ) = (
+ 'general1', 'memory1', 'performance2', 'performance1',
+ 'standard1', 'io1', 'onmetal', 'compute1',
+ )
+
+ # flavor classes that can be booted ONLY from volume
+ BFV_VOLUME_REQUIRED = {MEMORY1, COMPUTE1}
+
+ # flavor classes that can NOT be booted from volume
+ NON_BFV = {STANDARD1, ONMETAL}
+
properties_schema = copy.deepcopy(server.Server.properties_schema)
properties_schema.update(
{
@@ -219,6 +239,53 @@ class CloudServer(server.Server):
return self._extend_networks(nets)
+ def _image_flavor_class_match(self, flavor_type, image_obj):
+ flavor_class_string = image_obj.get(self.FLAVOR_CLASSES_KEY, '')
+ flavor_class_excluded = "!{0}".format(flavor_type)
+ flavor_classes_accepted = flavor_class_string.split(',')
+
+ if flavor_type in flavor_classes_accepted:
+ return True
+
+ if (self.FLAVOR_ACCEPT_ANY in flavor_classes_accepted and
+ flavor_class_excluded not in flavor_classes_accepted):
+ return True
+
+ return False
+
+ def validate(self):
+ """Validate for Rackspace Cloud specific parameters"""
+ super(CloudServer, self).validate()
+
+ # check if image, flavor combination is valid
+ flavor = self.properties[self.FLAVOR]
+ flavor_obj = self.client_plugin().get_flavor(flavor)
+ fl_xtra_specs = flavor_obj.to_dict().get(self.FLAVOR_EXTRA_SPECS, {})
+ flavor_type = fl_xtra_specs.get(self.FLAVOR_CLASS, None)
+
+ image = self.properties.get(self.IMAGE)
+ if not image:
+ if flavor_type in self.NON_BFV:
+ msg = _('Flavor %s cannot be booted from volume.') % flavor
+ raise exception.StackValidationFailed(message=msg)
+ else:
+ # we cannot determine details of the attached volume, so this
+ # is all the validation possible
+ return
+
+ image_obj = self.client_plugin('glance').get_image(image)
+
+ if not self._image_flavor_class_match(flavor_type, image_obj):
+ msg = _('Flavor %(flavor)s cannot be used with image '
+ '%(image)s.') % {'image': image, 'flavor': flavor}
+ raise exception.StackValidationFailed(message=msg)
+
+ if flavor_type in self.BFV_VOLUME_REQUIRED:
+ msg = _('Flavor %(flavor)s must be booted from volume, '
+ 'but image %(image)s was also specified.') % {
+ 'flavor': flavor, 'image': image}
+ raise exception.StackValidationFailed(message=msg)
+
def resource_mapping():
return {'OS::Nova::Server': CloudServer}
diff --git a/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py b/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py
index 6f534db9d..810fab554 100644
--- a/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py
+++ b/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py
@@ -21,6 +21,7 @@ from heat.common import exception
from heat.common import template_format
from heat.engine import environment
from heat.engine import resource
+from heat.engine import rsrc_defn
from heat.engine import scheduler
from heat.engine import stack as parser
from heat.engine import template
@@ -516,3 +517,134 @@ class CloudServersTest(common.HeatTestCase):
def test_server_no_user_data_software_config(self):
self._test_server_config_drive(None, False, True,
ud_format="SOFTWARE_CONFIG")
+
+
+@mock.patch.object(resource.Resource, "client_plugin")
+@mock.patch.object(resource.Resource, "client")
+class CloudServersValidationTests(common.HeatTestCase):
+ def setUp(self):
+ super(CloudServersValidationTests, self).setUp()
+ resource._register_class("OS::Nova::Server", cloud_server.CloudServer)
+ properties_server = {
+ "image": "CentOS 5.2",
+ "flavor": "256 MB Server",
+ "key_name": "test",
+ "user_data": "wordpress",
+ }
+ self.mockstack = mock.Mock()
+ self.mockstack.has_cache_data.return_value = False
+ self.mockstack.db_resource_get.return_value = None
+ self.rsrcdef = rsrc_defn.ResourceDefinition(
+ "test", cloud_server.CloudServer, properties=properties_server)
+
+ def test_validate_no_image(self, mock_client, mock_plugin):
+ properties_server = {
+ "flavor": "256 MB Server",
+ "key_name": "test",
+ "user_data": "wordpress",
+ }
+ rsrcdef = rsrc_defn.ResourceDefinition(
+ "test", cloud_server.CloudServer, properties=properties_server)
+
+ server = cloud_server.CloudServer("test", rsrcdef, self.mockstack)
+
+ mock_boot_vol = self.patchobject(
+ server, '_validate_block_device_mapping')
+ mock_boot_vol.return_value = True
+
+ self.assertIsNone(server.validate())
+
+ def test_validate_no_image_bfv(self, mock_client, mock_plugin):
+ properties_server = {
+ "flavor": "256 MB Server",
+ "key_name": "test",
+ "user_data": "wordpress",
+ }
+ rsrcdef = rsrc_defn.ResourceDefinition(
+ "test", cloud_server.CloudServer, properties=properties_server)
+
+ server = cloud_server.CloudServer("test", rsrcdef, self.mockstack)
+
+ mock_boot_vol = self.patchobject(
+ server, '_validate_block_device_mapping')
+ mock_boot_vol.return_value = True
+
+ mock_flavor = mock.Mock(ram=4)
+ mock_flavor.to_dict.return_value = {
+ 'OS-FLV-WITH-EXT-SPECS:extra_specs': {
+ 'class': 'standard1',
+ },
+ }
+
+ mock_plugin().get_flavor.return_value = mock_flavor
+
+ error = self.assertRaises(
+ exception.StackValidationFailed, server.validate)
+ self.assertEqual(
+ 'Flavor 256 MB Server cannot be booted from volume.',
+ six.text_type(error))
+
+ def test_validate_bfv_volume_only(self, mock_client, mock_plugin):
+ server = cloud_server.CloudServer("test", self.rsrcdef, self.mockstack)
+
+ mock_flavor = mock.Mock(ram=4, disk=4)
+ mock_flavor.to_dict.return_value = {
+ 'OS-FLV-WITH-EXT-SPECS:extra_specs': {
+ 'class': 'memory1',
+ },
+ }
+
+ mock_image = mock.Mock(status='ACTIVE', min_ram=2, min_disk=1)
+ mock_image.get.return_value = "memory1"
+
+ mock_plugin().get_flavor.return_value = mock_flavor
+ mock_plugin().get_image.return_value = mock_image
+
+ error = self.assertRaises(
+ exception.StackValidationFailed, server.validate)
+ self.assertEqual(
+ 'Flavor 256 MB Server must be booted from volume, '
+ 'but image CentOS 5.2 was also specified.',
+ six.text_type(error))
+
+ def test_validate_image_flavor_excluded_class(self, mock_client,
+ mock_plugin):
+ server = cloud_server.CloudServer("test", self.rsrcdef, self.mockstack)
+
+ mock_image = mock.Mock(status='ACTIVE', min_ram=2, min_disk=1)
+ mock_image.get.return_value = "!standard1, *"
+
+ mock_flavor = mock.Mock(ram=4, disk=4)
+ mock_flavor.to_dict.return_value = {
+ 'OS-FLV-WITH-EXT-SPECS:extra_specs': {
+ 'class': 'standard1',
+ },
+ }
+
+ mock_plugin().get_flavor.return_value = mock_flavor
+ mock_plugin().get_image.return_value = mock_image
+
+ error = self.assertRaises(
+ exception.StackValidationFailed, server.validate)
+ self.assertEqual(
+ 'Flavor 256 MB Server cannot be used with image CentOS 5.2.',
+ six.text_type(error))
+
+ def test_validate_image_flavor_ok(self, mock_client, mock_plugin):
+ server = cloud_server.CloudServer("test", self.rsrcdef, self.mockstack)
+
+ mock_image = mock.Mock(size=1, status='ACTIVE', min_ram=2, min_disk=2)
+ mock_image.get.return_value = "standard1"
+
+ mock_flavor = mock.Mock(ram=4, disk=4)
+ mock_flavor.to_dict.return_value = {
+ 'OS-FLV-WITH-EXT-SPECS:extra_specs': {
+ 'class': 'standard1',
+ 'disk_io_index': 1,
+ },
+ }
+
+ mock_plugin().get_flavor.return_value = mock_flavor
+ mock_plugin().get_image.return_value = mock_image
+
+ self.assertIsNone(server.validate())