]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Allow a Provider with a known facade its own schema
authorZane Bitter <zbitter@redhat.com>
Fri, 23 Aug 2013 13:56:41 +0000 (15:56 +0200)
committerZane Bitter <zbitter@redhat.com>
Fri, 23 Aug 2013 13:58:34 +0000 (15:58 +0200)
Allow a TemplateResource behind the facade of a known plugin to supply a
different schema to the facade resource. However, check during validation
that the two are basically compatible. The provider template must:

- Define parameters for all *required* properties of the facade.
- Define parameters that map to the same types as the facade properties.
- Not have *required* parameters that do not exist in the facade.
- Define outputs for all attributes of the facade.

Change-Id: Ie40ecbb43d3d6749266f2cb6d723c8537fcc23dd

heat/engine/properties.py
heat/engine/resources/template_resource.py
heat/tests/test_provider_template.py

index e12f2b277d1f5f5db7616e0791ff5cf178f72740..fa5ecaa5609cdcf07ef52df9e42a8f7789fae294 100644 (file)
@@ -584,6 +584,16 @@ class Property(object):
         return value
 
 
+def schemata(schema_dicts):
+    """
+    Return a dictionary of Schema objects for the given dictionary of schemata.
+
+    The input schemata are converted from the legacy (dictionary-based) format
+    to Schema objects where necessary.
+    """
+    return dict((n, Schema.from_legacy(s)) for n, s in schema_dicts.items())
+
+
 class Properties(collections.Mapping):
 
     def __init__(self, schema, data, resolver=lambda d: d, parent_name=None):
index 011334feca317709aaff9708c6394ff40496149b..30c20f489415fd51c6d940390747c282bb18c5cb 100644 (file)
@@ -15,6 +15,7 @@
 
 from requests import exceptions
 
+from heat.common import exception
 from heat.common import template_format
 from heat.common import urlfetch
 from heat.engine import attributes
@@ -45,24 +46,11 @@ class TemplateResource(stack_resource.StackResource):
             registry_type=environment.TemplateResourceInfo)
         self.template_name = tri.template_name
 
-        cri = stack.env.get_resource_info(
-            json_snippet['Type'],
-            registry_type=environment.ClassResourceInfo)
-
-        # if we're not overriding via the environment, mirror the template as
-        # a new resource
-        if cri is None or cri.get_class() == self.__class__:
-            tmpl = template.Template(self.parsed_nested)
-            self.properties_schema = (properties.Properties
-                .schema_from_params(tmpl.param_schemata()))
-            self.attributes_schema = (attributes.Attributes
-                .schema_from_outputs(tmpl[template.OUTPUTS]))
-        # otherwise we are overriding a resource type via the environment
-        # and should mimic that type
-        else:
-            cls_facade = cri.get_class()
-            self.properties_schema = cls_facade.properties_schema
-            self.attributes_schema = cls_facade.attributes_schema
+        tmpl = template.Template(self.parsed_nested)
+        self.properties_schema = (properties.Properties
+            .schema_from_params(tmpl.param_schemata()))
+        self.attributes_schema = (attributes.Attributes
+            .schema_from_outputs(tmpl[template.OUTPUTS]))
 
         super(TemplateResource, self).__init__(name, json_snippet, stack)
 
@@ -119,6 +107,47 @@ class TemplateResource(stack_resource.StackResource):
                 self.stack.t.files[self.template_name] = t_data
         return t_data
 
+    def validate(self):
+        cri = self.stack.env.get_resource_info(
+            self.type(),
+            registry_type=environment.ClassResourceInfo)
+
+        # If we're using an existing resource type as a facade for this
+        # template, check for compatibility between the interfaces.
+        if cri is not None and not isinstance(self, cri.get_class()):
+            facade_cls = cri.get_class()
+            facade_schemata = properties.schemata(facade_cls.properties_schema)
+
+            for n, fs in facade_schemata.items():
+                if fs.required and n not in self.properties_schema:
+                    msg = ("Required property %s for facade %s "
+                           "missing in provider") % (n, self.type())
+                    raise exception.StackValidationFailed(message=msg)
+
+                ps = self.properties_schema.get(n)
+                if (n in self.properties_schema and
+                        (fs.type != ps.type)):
+                    # Type mismatch
+                    msg = ("Property %s type mismatch between facade %s (%s) "
+                           "and provider (%s)") % (n, self.type(),
+                                                   fs.type, ps.type)
+                    raise exception.StackValidationFailed(message=msg)
+
+            for n, ps in self.properties_schema.items():
+                if ps.required and n not in facade_schemata:
+                    # Required property for template not present in facade
+                    msg = ("Provider requires property %s "
+                           "unknown in facade %s") % (n, self.type())
+                    raise exception.StackValidationFailed(message=msg)
+
+            for attr in facade_cls.attributes_schema:
+                if attr not in self.attributes_schema:
+                    msg = ("Attribute %s for facade %s "
+                           "missing in provider") % (attr, self.type())
+                    raise exception.StackValidationFailed(message=msg)
+
+        return super(TemplateResource, self).validate()
+
     def handle_create(self):
         return self.create_with_template(self.parsed_nested,
                                          self._to_parameters())
index 69cc9e4673487679921cbb9ca3e0b1e20a007c6c..d59d7542fcd5f3b2a325a47c69579a99fb796e66 100644 (file)
 #    under the License.
 
 import os
+import json
 
+from heat.common import exception
 from heat.common import urlfetch
 from heat.common import template_format
 
 from heat.engine import environment
 from heat.engine import parser
+from heat.engine import properties
 from heat.engine import resource
 from heat.engine.resources import template_resource
 
@@ -83,7 +86,19 @@ class ProviderTemplateTest(HeatTestCase):
 
     def test_to_parameters(self):
         """Tests property conversion to parameter values."""
-        utils.setup_dummy_db()
+        provider = {
+            'Parameters': {
+                'Foo': {'Type': 'String'},
+                'AList': {'Type': 'CommaDelimitedList'},
+                'ANum': {'Type': 'Number'},
+                'AMap': {'Type': 'Json'},
+            },
+            'Outputs': {
+                'Foo': {'Value': 'bar'},
+            },
+        }
+
+        files = {'test_resource.template': json.dumps(provider)}
 
         class DummyResource(object):
             attributes_schema = {"Foo": "A test attribute"}
@@ -99,7 +114,7 @@ class ProviderTemplateTest(HeatTestCase):
         env.load({'resource_registry':
                   {'DummyResource': 'test_resource.template'}})
         stack = parser.Stack(utils.dummy_context(), 'test_stack',
-                             parser.Template({}), env=env,
+                             parser.Template({}, files=files), env=env,
                              stack_id=uuidutils.generate_uuid())
 
         map_prop_val = {
@@ -119,10 +134,9 @@ class ProviderTemplateTest(HeatTestCase):
                 "AMap": map_prop_val
             }
         }
-        self.m.ReplayAll()
         temp_res = template_resource.TemplateResource('test_t_res',
                                                       json_snippet, stack)
-        self.m.VerifyAll()
+        temp_res.validate()
         converted_params = temp_res._to_parameters()
         self.assertTrue(converted_params)
         for key in DummyResource.properties_schema:
@@ -139,6 +153,191 @@ class ProviderTemplateTest(HeatTestCase):
         # verify Map conversion
         self.assertEqual(map_prop_val, converted_params.get("AMap"))
 
+    def test_attributes_extra(self):
+        provider = {
+            'Outputs': {
+                'Foo': {'Value': 'bar'},
+                'Blarg': {'Value': 'wibble'},
+            },
+        }
+        files = {'test_resource.template': json.dumps(provider)}
+
+        class DummyResource(object):
+            properties_schema = {}
+            attributes_schema = {"Foo": "A test attribute"}
+
+        env = environment.Environment()
+        resource._register_class('DummyResource', DummyResource)
+        env.load({'resource_registry':
+                  {'DummyResource': 'test_resource.template'}})
+        stack = parser.Stack(utils.dummy_context(), 'test_stack',
+                             parser.Template({}, files=files), env=env,
+                             stack_id=uuidutils.generate_uuid())
+
+        json_snippet = {
+            "Type": "DummyResource",
+        }
+
+        temp_res = template_resource.TemplateResource('test_t_res',
+                                                      json_snippet, stack)
+        self.assertEqual(None, temp_res.validate())
+
+    def test_attributes_missing(self):
+        provider = {
+            'Outputs': {
+                'Blarg': {'Value': 'wibble'},
+            },
+        }
+        files = {'test_resource.template': json.dumps(provider)}
+
+        class DummyResource(object):
+            properties_schema = {}
+            attributes_schema = {"Foo": "A test attribute"}
+
+        json_snippet = {
+            "Type": "DummyResource",
+        }
+
+        env = environment.Environment()
+        resource._register_class('DummyResource', DummyResource)
+        env.load({'resource_registry':
+                  {'DummyResource': 'test_resource.template'}})
+        stack = parser.Stack(utils.dummy_context(), 'test_stack',
+                             parser.Template({}, files=files), env=env,
+                             stack_id=uuidutils.generate_uuid())
+
+        temp_res = template_resource.TemplateResource('test_t_res',
+                                                      json_snippet, stack)
+        self.assertRaises(exception.StackValidationFailed,
+                          temp_res.validate)
+
+    def test_properties_normal(self):
+        provider = {
+            'Parameters': {
+                'Foo': {'Type': 'String'},
+                'Blarg': {'Type': 'String', 'Default': 'wibble'},
+            },
+        }
+        files = {'test_resource.template': json.dumps(provider)}
+
+        class DummyResource(object):
+            properties_schema = {"Foo": properties.Schema(properties.STRING,
+                                                          required=True)}
+            attributes_schema = {}
+
+        json_snippet = {
+            "Type": "DummyResource",
+            "Properties": {
+                "Foo": "bar",
+            },
+        }
+
+        env = environment.Environment()
+        resource._register_class('DummyResource', DummyResource)
+        env.load({'resource_registry':
+                  {'DummyResource': 'test_resource.template'}})
+        stack = parser.Stack(utils.dummy_context(), 'test_stack',
+                             parser.Template({}, files=files), env=env,
+                             stack_id=uuidutils.generate_uuid())
+
+        temp_res = template_resource.TemplateResource('test_t_res',
+                                                      json_snippet, stack)
+        self.assertEqual(None, temp_res.validate())
+
+    def test_properties_missing(self):
+        provider = {
+            'Parameters': {
+                'Blarg': {'Type': 'String', 'Default': 'wibble'},
+            },
+        }
+        files = {'test_resource.template': json.dumps(provider)}
+
+        class DummyResource(object):
+            properties_schema = {"Foo": properties.Schema(properties.STRING,
+                                                          required=True)}
+            attributes_schema = {}
+
+        json_snippet = {
+            "Type": "DummyResource",
+        }
+
+        env = environment.Environment()
+        resource._register_class('DummyResource', DummyResource)
+        env.load({'resource_registry':
+                  {'DummyResource': 'test_resource.template'}})
+        stack = parser.Stack(utils.dummy_context(), 'test_stack',
+                             parser.Template({}, files=files), env=env,
+                             stack_id=uuidutils.generate_uuid())
+
+        temp_res = template_resource.TemplateResource('test_t_res',
+                                                      json_snippet, stack)
+        self.assertRaises(exception.StackValidationFailed,
+                          temp_res.validate)
+
+    def test_properties_extra_required(self):
+        provider = {
+            'Parameters': {
+                'Blarg': {'Type': 'String'},
+            },
+        }
+        files = {'test_resource.template': json.dumps(provider)}
+
+        class DummyResource(object):
+            properties_schema = {}
+            attributes_schema = {}
+
+        json_snippet = {
+            "Type": "DummyResource",
+            "Properties": {
+                "Blarg": "wibble",
+            },
+        }
+
+        env = environment.Environment()
+        resource._register_class('DummyResource', DummyResource)
+        env.load({'resource_registry':
+                  {'DummyResource': 'test_resource.template'}})
+        stack = parser.Stack(utils.dummy_context(), 'test_stack',
+                             parser.Template({}, files=files), env=env,
+                             stack_id=uuidutils.generate_uuid())
+
+        temp_res = template_resource.TemplateResource('test_t_res',
+                                                      json_snippet, stack)
+        self.assertRaises(exception.StackValidationFailed,
+                          temp_res.validate)
+
+    def test_properties_type_mismatch(self):
+        provider = {
+            'Parameters': {
+                'Foo': {'Type': 'String'},
+            },
+        }
+        files = {'test_resource.template': json.dumps(provider)}
+
+        class DummyResource(object):
+            properties_schema = {"Foo": properties.Schema(properties.MAP)}
+            attributes_schema = {}
+
+        json_snippet = {
+            "Type": "DummyResource",
+            "Properties": {
+                "Foo": "bar",
+            },
+        }
+
+        env = environment.Environment()
+        resource._register_class('DummyResource', DummyResource)
+        env.load({'resource_registry':
+                  {'DummyResource': 'test_resource.template'}})
+        stack = parser.Stack(utils.dummy_context(), 'test_stack',
+                             parser.Template({}, files=files), env=env,
+                             stack_id=uuidutils.generate_uuid())
+
+        temp_res = template_resource.TemplateResource('test_t_res',
+                                                      json_snippet, stack)
+        self.assertRaises(exception.StackValidationFailed,
+                          temp_res.validate)
+
     def test_get_template_resource(self):
         # assertion: if the name matches {.yaml|.template} we get the
         # TemplateResource class.