Introduce a schema for attributes

Similar to properties, adds attribute_schema and attributes members to
Resources in order to facilitate document generation and template
provider stubs for resources.

Change-Id: I5e207360816fbc685c66db68a7fab8afad11bf10
Implements: blueprint attributes-schema
This commit is contained in:
Randall Burt 2013-06-13 12:41:50 -05:00
parent 8805221195
commit 42de0c9a3c
4 changed files with 275 additions and 1 deletions

135
heat/engine/attributes.py Normal file
View File

@ -0,0 +1,135 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
class Attribute(object):
"""
An attribute description and resolved value.
:param resource_name: the logical name of the resource having this
attribute
:param attr_name: the name of the attribute
:param description: attribute description
:param resolver: a function that will resolve the value of this attribute
"""
def __init__(self, attr_name, description, resolver):
self._name = attr_name
self._description = description
self._resolve = resolver
@property
def name(self):
"""
:returns: The attribute name
"""
return self._name
@property
def value(self):
"""
:returns: The resolved attribute value
"""
return self._resolve(self._name)
@property
def description(self):
"""
:returns: A description of the attribute
"""
return self._description
@staticmethod
def as_output(resource_name, attr_name, description):
"""
:param resource_name: the logical name of a resource
:param attr_name: the name of the attribute
:description: the description of the attribute
:returns: This attribute as a template 'Output' entry
"""
return {
attr_name: {
"Value": '{"Fn::GetAtt": ["%s", "%s"]}' % (resource_name,
attr_name),
"Description": description
}
}
def __call__(self):
return self.value
def __str__(self):
return ("Attribute %s: %s" % (self.name, self.value))
class Attributes(collections.Mapping):
"""Models a collection of Resource Attributes."""
def __init__(self, res_name, schema, resolver):
self._resource_name = res_name
self._attributes = dict((k, Attribute(k, v, resolver))
for k, v in schema.items())
@property
def attributes(self):
"""
Get a copy of the attribute definitions in this collection
(as opposed to attribute values); useful for doc and
template format generation
:returns: attribute definitions
"""
# return a deep copy to avoid modification
return dict((k, Attribute(k, v.description, v._resolve)) for k, v
in self._attributes.items())
@staticmethod
def as_outputs(resource_name, resource_class):
"""
:param resource_name: logical name of the resource
:param resource_class: resource implementation class
:returns: The attributes of the specified resource_class as a template
Output map
"""
outputs = {}
for name, descr in resource_class.attributes_schema.items():
outputs.update(Attribute.as_output(resource_name, name, descr))
return outputs
@staticmethod
def schema_from_outputs(json_snippet):
return dict(("Outputs.%s" % k, v.get("Description"))
for k, v in json_snippet.items())
def __getitem__(self, key):
if key not in self:
raise KeyError('%s: Invalid attribute %s' %
(self._resource_name, key))
return self._attributes[key]()
def __len__(self):
return len(self._attributes)
def __contains__(self, key):
return key in self._attributes
def __iter__(self):
return iter(self._attributes)
def __repr__(self):
return ("Attributes for %s:\n\t" % self._resource_name +
'\n\t'.join(self._attributes.values()))

View File

@ -23,6 +23,8 @@ from heat.db import api as db_api
from heat.common import identifier from heat.common import identifier
from heat.common import short_id from heat.common import short_id
from heat.engine import timestamp from heat.engine import timestamp
# import class to avoid name collisions and ugly aliasing
from heat.engine.attributes import Attributes
from heat.engine.properties import Properties from heat.engine.properties import Properties
from heat.openstack.common import log as logging from heat.openstack.common import log as logging
@ -122,6 +124,10 @@ class Resource(object):
# supported for handle_update, used by update_template_diff_properties # supported for handle_update, used by update_template_diff_properties
update_allowed_properties = () update_allowed_properties = ()
# Resource implementations set this to the name: description dictionary
# that describes the appropriate resource attributes
attributes_schema = {}
def __new__(cls, name, json, stack): def __new__(cls, name, json, stack):
'''Create a new Resource of the appropriate class for its type.''' '''Create a new Resource of the appropriate class for its type.'''
@ -149,6 +155,9 @@ class Resource(object):
self.t.get('Properties', {}), self.t.get('Properties', {}),
self.stack.resolve_runtime_data, self.stack.resolve_runtime_data,
self.name) self.name)
self.attributes = Attributes(self.name,
self.attributes_schema,
self._resolve_attribute)
resource = db_api.resource_get_by_name_and_stack(self.context, resource = db_api.resource_get_by_name_and_stack(self.context,
name, stack.id) name, stack.id)
@ -570,6 +579,17 @@ class Resource(object):
elif (action, status) == (self.CREATE, self.IN_PROGRESS): elif (action, status) == (self.CREATE, self.IN_PROGRESS):
self._store() self._store()
def _resolve_attribute(self, name):
"""
Default implementation; should be overridden by resources that expose
attributes
:param name: The attribute to resolve
:returns: the resource attribute named key
"""
# By default, no attributes resolve
pass
def state_set(self, action, status, reason="state changed"): def state_set(self, action, status, reason="state changed"):
if action not in self.ACTIONS: if action not in self.ACTIONS:
raise ValueError("Invalid action %s" % action) raise ValueError("Invalid action %s" % action)
@ -604,7 +624,11 @@ class Resource(object):
http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/\ http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/\
intrinsic-function-reference-getatt.html intrinsic-function-reference-getatt.html
''' '''
return unicode(self.name) try:
return self.attributes[key]
except KeyError:
raise exception.InvalidTemplateAttribute(resource=self.name,
key=key)
def FnBase64(self, data): def FnBase64(self, data):
''' '''

View File

@ -24,9 +24,14 @@ class GenericResource(resource.Resource):
Dummy resource for use in tests Dummy resource for use in tests
''' '''
properties_schema = {} properties_schema = {}
attributes_schema = {'foo': 'A generic attribute',
'Foo': 'Another generic attribute'}
def handle_create(self): def handle_create(self):
logger.warning('Creating generic resource (Type "%s")' % self.type()) logger.warning('Creating generic resource (Type "%s")' % self.type())
def handle_update(self, json_snippet, tmpl_diff, prop_diff): def handle_update(self, json_snippet, tmpl_diff, prop_diff):
logger.warning('Updating generic resource (Type "%s")' % self.type()) logger.warning('Updating generic resource (Type "%s")' % self.type())
def _resolve_attribute(self, name):
return self.name

View File

@ -0,0 +1,110 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mox
from heat.engine import attributes
from heat.tests import common
test_attribute_schema = {
"attribute1": "A description for attribute 1",
"attribute2": "A description for attribute 2",
"another attribute": "The last attribute"
}
class AttributeTest(common.HeatTestCase):
"""Test the Attribute class."""
def setUp(self):
common.HeatTestCase.setUp(self)
self.test_resolver = self.m.CreateMockAnything()
def test_resolve_attribute(self):
"""Test that an Attribute returns a good value based on resolver."""
test_val = "test value"
# resolved with a good value first
self.test_resolver('test').AndReturn('test value')
# second call resolves to None
self.test_resolver(mox.IgnoreArg()).AndReturn(None)
self.m.ReplayAll()
test_attr = attributes.Attribute("test", "A test attribute",
self.test_resolver)
self.assertEqual(test_val, test_attr.value,
"Unexpected attribute value")
self.assertIsNone(test_attr.value,
"Second attrib value should be None")
def test_as_output(self):
"""Test that Attribute looks right when viewed as an Output."""
expected = {
"test1": {
"Value": '{"Fn::GetAtt": ["test_resource", "test1"]}',
"Description": "The first test attribute"
}
}
self.assertEqual(expected,
attributes.Attribute.as_output(
"test_resource",
"test1",
"The first test attribute"),
'Attribute as Output mismatch')
class AttributesTest(common.HeatTestCase):
"""Test the Attributes class."""
attributes_schema = {
"test1": "Test attrib 1",
"test2": "Test attrib 2",
"test3": "Test attrib 3"
}
def test_get_attribute(self):
"""Test that we get the attribute values we expect."""
test_resolver = lambda x: "value1"
self.m.ReplayAll()
attribs = attributes.Attributes('test resource',
self.attributes_schema,
test_resolver)
self.assertEqual("value1", attribs['test1'])
self.assertRaises(KeyError, attribs.__getitem__, 'not there')
def test_as_outputs(self):
"""Test that Output format works as expected."""
expected = {
"test1": {
"Value": '{"Fn::GetAtt": ["test_resource", "test1"]}',
"Description": "Test attrib 1"
},
"test2": {
"Value": '{"Fn::GetAtt": ["test_resource", "test2"]}',
"Description": "Test attrib 2"
},
"test3": {
"Value": '{"Fn::GetAtt": ["test_resource", "test3"]}',
"Description": "Test attrib 3"
}
}
MyTestResourceClass = self.m.CreateMockAnything()
MyTestResourceClass.attributes_schema = {
"test1": "Test attrib 1",
"test2": "Test attrib 2",
"test3": "Test attrib 3"
}
self.m.ReplayAll()
self.assertEqual(
expected,
attributes.Attributes.as_outputs("test_resource",
MyTestResourceClass))