From: Zane Bitter Date: Fri, 23 Aug 2013 13:56:41 +0000 (+0200) Subject: Allow a Provider with a known facade its own schema X-Git-Tag: 2014.1~146^2 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=26488c8b2e16d097ae070d6d01078dff624e5c98;p=openstack-build%2Fheat-build.git Allow a Provider with a known facade its own schema 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 --- diff --git a/heat/engine/properties.py b/heat/engine/properties.py index e12f2b27..fa5ecaa5 100644 --- a/heat/engine/properties.py +++ b/heat/engine/properties.py @@ -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): diff --git a/heat/engine/resources/template_resource.py b/heat/engine/resources/template_resource.py index 011334fe..30c20f48 100644 --- a/heat/engine/resources/template_resource.py +++ b/heat/engine/resources/template_resource.py @@ -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()) diff --git a/heat/tests/test_provider_template.py b/heat/tests/test_provider_template.py index 69cc9e46..d59d7542 100644 --- a/heat/tests/test_provider_template.py +++ b/heat/tests/test_provider_template.py @@ -13,12 +13,15 @@ # 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.