Add type field to the resource attributes schema
Add data type for the attributes. This will help 1) validating the attibute against its type 2) resource-schema api will show the attribute type 3) template authors will know what type of value to expect making indexing and mapping easier 4) generating docs for the attribute type Implements: blueprint add-type-in-attributes-schema Change-Id: Ifc92c57ec1ddd2ab5f810587a1d33e762308dd8a
This commit is contained in:
parent
477d331f67
commit
cafd53be11
|
@ -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
|
||||
|
|
|
@ -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
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
@ -1020,7 +1020,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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -2194,6 +2194,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,
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue