]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Introduce a schema for attributes
authorRandall Burt <randall.burt@rackspace.com>
Thu, 13 Jun 2013 17:41:50 +0000 (12:41 -0500)
committerRandall Burt <randall.burt@rackspace.com>
Wed, 19 Jun 2013 22:51:03 +0000 (17:51 -0500)
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

heat/engine/attributes.py [new file with mode: 0644]
heat/engine/resource.py
heat/tests/generic_resource.py
heat/tests/test_attributes.py [new file with mode: 0644]

diff --git a/heat/engine/attributes.py b/heat/engine/attributes.py
new file mode 100644 (file)
index 0000000..d5e0811
--- /dev/null
@@ -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()))
index 9cb7cb6252653359bcf7a826c27b5fa6f7621539..a77c20ea40334dc033c6370c45b81959c32b67ba 100644 (file)
@@ -23,6 +23,8 @@ from heat.db import api as db_api
 from heat.common import identifier
 from heat.common import short_id
 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.openstack.common import log as logging
@@ -122,6 +124,10 @@ class Resource(object):
     # supported for handle_update, used by update_template_diff_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):
         '''Create a new Resource of the appropriate class for its type.'''
 
@@ -149,6 +155,9 @@ class Resource(object):
                                      self.t.get('Properties', {}),
                                      self.stack.resolve_runtime_data,
                                      self.name)
+        self.attributes = Attributes(self.name,
+                                     self.attributes_schema,
+                                     self._resolve_attribute)
 
         resource = db_api.resource_get_by_name_and_stack(self.context,
                                                          name, stack.id)
@@ -570,6 +579,17 @@ class Resource(object):
         elif (action, status) == (self.CREATE, self.IN_PROGRESS):
             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"):
         if action not in self.ACTIONS:
             raise ValueError("Invalid action %s" % action)
@@ -604,7 +624,11 @@ class Resource(object):
         http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/\
         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):
         '''
index 7cd267c4b5b2940b1d9f32d02098ed179a804e0d..a6b7dd77358aa702d5af593e141e8a567d367133 100644 (file)
@@ -24,9 +24,14 @@ class GenericResource(resource.Resource):
     Dummy resource for use in tests
     '''
     properties_schema = {}
+    attributes_schema = {'foo': 'A generic attribute',
+                         'Foo': 'Another generic attribute'}
 
     def handle_create(self):
         logger.warning('Creating generic resource (Type "%s")' % self.type())
 
     def handle_update(self, json_snippet, tmpl_diff, prop_diff):
         logger.warning('Updating generic resource (Type "%s")' % self.type())
+
+    def _resolve_attribute(self, name):
+        return self.name
diff --git a/heat/tests/test_attributes.py b/heat/tests/test_attributes.py
new file mode 100644 (file)
index 0000000..a1e3ca8
--- /dev/null
@@ -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))