Merge "Add image/flavor validation to Rackspace Server"

This commit is contained in:
Jenkins 2016-03-02 21:53:11 +00:00 committed by Gerrit Code Review
commit f87c59b9c2
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())