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>
This commit is contained in:
Pratik Mallya 2015-12-16 17:34:56 -06:00 committed by Jason Dunsmore
parent dc1bec455a
commit 006db59851
2 changed files with 199 additions and 0 deletions

View File

@ -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}

View File

@ -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())