diff options
Diffstat (limited to 'contrib')
-rw-r--r-- | contrib/rackspace/rackspace/resources/cloud_server.py | 67 | ||||
-rw-r--r-- | contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py | 132 |
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()) |