From d8fefed1773133bbdf58a3392bafbc12f82e4fe2 Mon Sep 17 00:00:00 2001 From: Jason Dunsmore Date: Wed, 13 Apr 2016 15:19:44 -0500 Subject: [PATCH] Fix Cloud Server image/flavor combination validation Custom Cloud Server images do not have the 'flavor_classes' key in their metadata, so try to get it from the base image. If 'flavor_classes' is empty or does not exist, skip image/flavor validation. Closes-Bug: #1570087 Change-Id: I44fd02c689ba0c06ba571d04e9baf0eb0ce035ff --- .../rackspace/resources/cloud_server.py | 23 +++++-- .../tests/test_rackspace_cloud_server.py | 60 +++++++++++++++++++ 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/contrib/rackspace/rackspace/resources/cloud_server.py b/contrib/rackspace/rackspace/resources/cloud_server.py index c59f6468c9..2be722f7f8 100644 --- a/contrib/rackspace/rackspace/resources/cloud_server.py +++ b/contrib/rackspace/rackspace/resources/cloud_server.py @@ -68,6 +68,7 @@ class CloudServer(server.Server): 'general1', 'memory1', 'performance2', 'performance1', 'standard1', 'io1', 'onmetal', 'compute1', ) + BASE_IMAGE_REF = 'base_image_ref' # flavor classes that can be booted ONLY from volume BFV_VOLUME_REQUIRED = {MEMORY1, COMPUTE1} @@ -239,8 +240,22 @@ 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, '') + def _base_image_obj(self, image): + image_obj = self.client_plugin('glance').get_image(image) + if self.BASE_IMAGE_REF in image_obj: + base_image = image_obj[self.BASE_IMAGE_REF] + return self.client_plugin('glance').get_image(base_image) + return image_obj + + def _image_flavor_class_match(self, flavor_type, image): + base_image_obj = self._base_image_obj(image) + flavor_class_string = base_image_obj.get(self.FLAVOR_CLASSES_KEY) + + # If the flavor_class_string metadata does not exist or is + # empty, do not validate image/flavor combo + if not flavor_class_string: + return True + flavor_class_excluded = "!{0}".format(flavor_type) flavor_classes_accepted = flavor_class_string.split(',') @@ -273,9 +288,7 @@ class CloudServer(server.Server): # 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): + if not self._image_flavor_class_match(flavor_type, image): msg = _('Flavor %(flavor)s cannot be used with image ' '%(image)s.') % {'image': image, 'flavor': flavor} raise exception.StackValidationFailed(message=msg) diff --git a/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py b/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py index 5cb1bb8556..941b1f6f0d 100644 --- a/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py +++ b/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py @@ -545,6 +545,7 @@ class CloudServersValidationTests(common.HeatTestCase): mock_image = mock.Mock(status='ACTIVE', min_ram=2, min_disk=1) mock_image.get.return_value = "memory1" + mock_image.__iter__ = mock.Mock(return_value=iter([])) mock_plugin().get_flavor.return_value = mock_flavor mock_plugin().get_image.return_value = mock_image @@ -564,6 +565,7 @@ class CloudServersValidationTests(common.HeatTestCase): mock_image = mock.Mock(status='ACTIVE', min_ram=2, min_disk=1) mock_image.get.return_value = "!standard1, *" + mock_image.__iter__ = mock.Mock(return_value=iter([])) mock_flavor = mock.Mock(ram=4, disk=4) mock_flavor.to_dict.return_value = { @@ -588,6 +590,7 @@ class CloudServersValidationTests(common.HeatTestCase): mock_image = mock.Mock(size=1, status='ACTIVE', min_ram=2, min_disk=2) mock_image.get.return_value = "standard1" + mock_image.__iter__ = mock.Mock(return_value=iter([])) mock_flavor = mock.Mock(ram=4, disk=4) mock_flavor.to_dict.return_value = { @@ -601,3 +604,60 @@ class CloudServersValidationTests(common.HeatTestCase): mock_plugin().get_image.return_value = mock_image self.assertIsNone(server.validate()) + + def test_validate_image_flavor_empty_metadata(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 = "" + mock_image.__iter__ = mock.Mock(return_value=iter([])) + + mock_flavor = mock.Mock(ram=4, disk=4) + mock_flavor.to_dict.return_value = { + 'OS-FLV-WITH-EXT-SPECS:extra_specs': { + 'flavor_classes': '', + }, + } + + mock_plugin().get_flavor.return_value = mock_flavor + mock_plugin().get_image.return_value = mock_image + + self.assertIsNone(server.validate()) + + def test_validate_image_flavor_no_metadata(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 = None + mock_image.__iter__ = mock.Mock(return_value=iter([])) + + mock_flavor = mock.Mock(ram=4, disk=4) + mock_flavor.to_dict.return_value = {} + + mock_plugin().get_flavor.return_value = mock_flavor + mock_plugin().get_image.return_value = mock_image + + self.assertIsNone(server.validate()) + + def test_validate_image_flavor_not_base(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 = None + mock_image.__iter__ = mock.Mock(return_value=iter( + ['base_image_ref'])) + mock_image.__getitem__ = mock.Mock(return_value='1234') + + mock_base_image = mock.Mock(size=1, status='ACTIVE', min_ram=2, + min_disk=2) + mock_base_image.get.return_value = None + mock_base_image.__iter__ = mock.Mock(return_value=iter([])) + + mock_flavor = mock.Mock(ram=4, disk=4) + mock_flavor.to_dict.return_value = {} + + mock_plugin().get_flavor.return_value = mock_flavor + mock_plugin().get_image.side_effect = [mock_image, mock_base_image] + + self.assertIsNone(server.validate())