summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2016-03-02 21:53:11 +0000
committerGerrit Code Review <review@openstack.org>2016-03-02 21:53:12 +0000
commitf87c59b9c21f3b310f10f89cd3b6b5667d9a72f5 (patch)
treee5dad2516193ba23ce506d38a623d76151686eb1 /contrib
parentedeae75de4a8bb45b76365c0c3e750ef21c15263 (diff)
parent006db59851a43838b4f05b43d654f4dd8da7c188 (diff)
downloadheat-f87c59b9c21f3b310f10f89cd3b6b5667d9a72f5.tar.gz
Merge "Add image/flavor validation to Rackspace Server"
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())