Merge "Add type field to the resource attributes schema"

This commit is contained in:
Jenkins 2015-05-07 10:53:55 +00:00 committed by Gerrit Code Review
commit d047c865ac
7 changed files with 147 additions and 12 deletions

View File

@ -20,6 +20,10 @@ from heat.common.i18n import _
from heat.engine import constraints as constr
from heat.engine import support
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
class Schema(constr.Schema):
"""
@ -30,9 +34,9 @@ class Schema(constr.Schema):
"""
KEYS = (
DESCRIPTION,
DESCRIPTION, TYPE
) = (
'description',
'description', 'type',
)
CACHE_MODES = (
@ -43,18 +47,30 @@ class Schema(constr.Schema):
'cache_none'
)
TYPES = (
STRING, MAP, LIST,
) = (
'String', 'Map', 'List',
)
def __init__(self, description=None,
support_status=support.SupportStatus(),
cache_mode=CACHE_LOCAL):
cache_mode=CACHE_LOCAL,
type=None):
self.description = description
self.support_status = support_status
self.cache_mode = cache_mode
self.type = type
def __getitem__(self, key):
if key == self.DESCRIPTION:
if self.description is not None:
return self.description
elif key == self.TYPE:
if self.type is not None:
return self.type.lower()
raise KeyError(key)
@classmethod
@ -156,6 +172,24 @@ class Attributes(collections.Mapping):
for k, v in json_snippet.items())
return {}
def _validate_type(self, attrib, value):
if attrib.schema.type == attrib.schema.STRING:
if not isinstance(value, six.string_types):
LOG.warn(_("Attribute %(name)s is not of type %(att_type)s"),
{'name': attrib.name,
'att_type': attrib.schema.STRING})
elif attrib.schema.type == attrib.schema.LIST:
if (not isinstance(value, collections.Sequence)
or isinstance(value, six.string_types)):
LOG.warn(_("Attribute %(name)s is not of type %(att_type)s"),
{'name': attrib.name,
'att_type': attrib.schema.LIST})
elif attrib.schema.type == attrib.schema.MAP:
if not isinstance(value, collections.Mapping):
LOG.warn(_("Attribute %(name)s is not of type %(att_type)s"),
{'name': attrib.name,
'att_type': attrib.schema.MAP})
def __getitem__(self, key):
if key not in self:
raise KeyError(_('%(resource)s: Invalid attribute %(key)s') %
@ -169,7 +203,10 @@ class Attributes(collections.Mapping):
return self._resolved_values[key]
value = self._resolver(key)
if value is not None:
# validate the value against its type
self._validate_type(attrib, value)
# only store if not None, it may resolve to an actual value
# on subsequent calls
self._resolved_values[key] = value

View File

@ -430,19 +430,23 @@ class Server(stack_user.StackUser):
attributes_schema = {
NAME_ATTR: attributes.Schema(
_('Name of the server.')
_('Name of the server.'),
type=attributes.Schema.STRING
),
SHOW: attributes.Schema(
_('A dict of all server details as returned by the API.')
_('A dict of all server details as returned by the API.'),
type=attributes.Schema.MAP
),
ADDRESSES: attributes.Schema(
_('A dict of all network addresses with corresponding port_id. '
'The port ID may be obtained through the following expression: '
'"{get_attr: [<server>, addresses, <network name>, 0, port]}".')
'"{get_attr: [<server>, addresses, <network name>, 0, port]}".'),
type=attributes.Schema.MAP
),
NETWORKS_ATTR: attributes.Schema(
_('A dict of assigned network addresses of the form: '
'{"public": [ip1, ip2...], "private": [ip3, ip4]}.')
'{"public": [ip1, ip2...], "private": [ip3, ip4]}.'),
type=attributes.Schema.MAP
),
FIRST_ADDRESS: attributes.Schema(
_('Convenience attribute to fetch the first assigned network '
@ -458,15 +462,18 @@ class Server(stack_user.StackUser):
)
),
INSTANCE_NAME: attributes.Schema(
_('AWS compatible instance name.')
_('AWS compatible instance name.'),
type=attributes.Schema.STRING
),
ACCESSIPV4: attributes.Schema(
_('The manually assigned alternative public IPv4 address '
'of the server.')
'of the server.'),
type=attributes.Schema.STRING
),
ACCESSIPV6: attributes.Schema(
_('The manually assigned alternative public IPv6 address '
'of the server.')
'of the server.'),
type=attributes.Schema.STRING
),
CONSOLE_URLS: attributes.Schema(
_("URLs of server's consoles. "
@ -475,7 +482,8 @@ class Server(stack_user.StackUser):
"e.g. get_attr: [ <server>, console_urls, novnc ]. "
"Currently supported types are "
"novnc, xvpvnc, spice-html5, rdp-html5, serial."),
support_status=support.SupportStatus(version='2015.1')
support_status=support.SupportStatus(version='2015.1'),
type=attributes.Schema.MAP
),
}

View File

@ -1042,7 +1042,7 @@ class EngineService(service.Service):
def attributes_schema():
for name, schema_data in resource_class.attributes_schema.items():
schema = attributes.Schema.from_attribute(schema_data)
yield name, {schema.DESCRIPTION: schema.description}
yield name, dict(schema)
return {
rpc_api.RES_SCHEMA_RES_TYPE: type_name,

View File

@ -162,3 +162,18 @@ class ResourceWithCustomConstraint(GenericResource):
'Foo': properties.Schema(
properties.Schema.STRING,
constraints=[constraints.CustomConstraint('neutron.network')])}
class ResourceWithAttributeType(GenericResource):
attributes_schema = {
'attr1': attributes.Schema('A generic attribute',
type=attributes.Schema.STRING),
'attr2': attributes.Schema('Another generic attribute',
type=attributes.Schema.MAP)
}
def _resolve_attribute(self, name):
if name == 'attr1':
return "valid_sting"
elif name == 'attr2':
return "invalid_type"

View File

@ -26,6 +26,12 @@ class AttributeSchemaTest(common.HeatTestCase):
s = attributes.Schema('A attribute')
self.assertEqual(d, dict(s))
d = {'description': 'Another attribute',
'type': 'string'}
s = attributes.Schema('Another attribute',
type=attributes.Schema.STRING)
self.assertEqual(d, dict(s))
def test_all_resource_schemata(self):
for resource_type in resources.global_env().get_types():
for schema in six.itervalues(getattr(resource_type,
@ -39,6 +45,12 @@ class AttributeSchemaTest(common.HeatTestCase):
self.assertEqual('Test description.',
attributes.Schema.from_attribute(s).description)
s = attributes.Schema('Test description.',
type=attributes.Schema.MAP)
self.assertIs(s, attributes.Schema.from_attribute(s))
self.assertEqual(attributes.Schema.MAP,
attributes.Schema.from_attribute(s).type)
def test_schema_support_status(self):
schema = {
'foo_sup': attributes.Schema(
@ -176,3 +188,29 @@ class AttributesTest(common.HeatTestCase):
self.assertEqual("value3", attribs['test3'])
value = 'value3 changed'
self.assertEqual("value3 changed", attribs['test3'])
def test_validate_type_invalid(self):
resolver = mock.Mock()
# Test invalid string type attribute
attr_schema = attributes.Schema("Test attribute",
type=attributes.Schema.STRING)
attr = attributes.Attribute("test1", attr_schema)
attribs = attributes.Attributes('test resource', attr_schema, resolver)
attribs._validate_type(attr, [])
self.assertIn("Attribute test1 is not of type String", self.LOG.output)
# Test invalid list type attribute
attr_schema = attributes.Schema("Test attribute",
type=attributes.Schema.LIST)
attr = attributes.Attribute("test1", attr_schema)
attribs = attributes.Attributes('test resource', attr_schema, resolver)
attribs._validate_type(attr, 'invalid')
self.assertIn("Attribute test1 is not of type List", self.LOG.output)
# Test invalid map type attribute
attr_schema = attributes.Schema("Test attribute",
type=attributes.Schema.MAP)
attr = attributes.Attribute("test1", attr_schema)
attribs = attributes.Attributes('test resource', attr_schema, resolver)
attribs._validate_type(attr, 'invalid')
self.assertIn("Attribute test1 is not of type Map", self.LOG.output)

View File

@ -2629,6 +2629,24 @@ class StackServiceTest(common.HeatTestCase):
schema = self.eng.resource_schema(self.ctx, type_name=type_name)
self.assertEqual(expected, schema)
def test_resource_schema_with_attr_type(self):
res._register_class('ResourceWithAttributeType',
generic_rsrc.ResourceWithAttributeType)
type_name = 'ResourceWithAttributeType'
expected = {
'resource_type': type_name,
'properties': {},
'attributes': {
'attr1': {'description': 'A generic attribute',
'type': 'string'},
'attr2': {'description': 'Another generic attribute',
'type': 'map'},
},
}
schema = self.eng.resource_schema(self.ctx, type_name=type_name)
self.assertEqual(expected, schema)
def _no_template_file(self, function):
env = environment.Environment()
info = environment.ResourceInfo(env.registry,

View File

@ -1285,6 +1285,25 @@ class ResourceTest(common.HeatTestCase):
property_schema_name, property_schema,
res_class.__name__)
def test_getatt_invalid_type(self):
resource._register_class('ResourceWithAttributeType',
generic_rsrc.ResourceWithAttributeType)
tmpl = template.Template({
'heat_template_version': '2013-05-23',
'resources': {
'res': {
'type': 'ResourceWithAttributeType'
}
}
})
stack = parser.Stack(utils.dummy_context(), 'test', tmpl)
res = stack['res']
self.assertEqual('valid_sting', res.FnGetAtt('attr1'))
res.FnGetAtt('attr2')
self.assertIn("Attribute attr2 is not of type Map", self.LOG.output)
class ResourceAdoptTest(common.HeatTestCase):
def setUp(self):