Merge "Add image/flavor validation to Rackspace Server"
This commit is contained in:
commit
f87c59b9c2
@ -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}
|
||||
|
@ -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())
|
||||
|
Loading…
x
Reference in New Issue
Block a user