From 401fd430d1df9ec71ed55979db87c0c631ff2290 Mon Sep 17 00:00:00 2001 From: Sergey Kraynev Date: Wed, 4 Mar 2015 11:39:31 -0500 Subject: [PATCH] Add way to collect map of needed attributes Change-Id: I36cd2d418d570da3a0aa9df5408442251a400978 Implements: blueprint convergence-push-data --- heat/engine/cfn/functions.py | 8 + heat/engine/function.py | 25 +++ heat/engine/resource.py | 3 + heat/engine/rsrc_defn.py | 10 ++ heat/engine/stack.py | 14 ++ heat/tests/test_stack_collect_attributes.py | 182 ++++++++++++++++++++ 6 files changed, 242 insertions(+) create mode 100644 heat/tests/test_stack_collect_attributes.py diff --git a/heat/engine/cfn/functions.py b/heat/engine/cfn/functions.py index dc7fc1906..0ab287c9c 100644 --- a/heat/engine/cfn/functions.py +++ b/heat/engine/cfn/functions.py @@ -171,6 +171,14 @@ class GetAtt(function.Function): raise exception.InvalidTemplateReference(resource=resource_name, key=path) + def dep_attrs(self, resource_name): + if self._resource().name == resource_name: + attrs = [function.resolve(self._attribute)] + else: + attrs = [] + return itertools.chain(super(GetAtt, self).dep_attrs(resource_name), + attrs) + def dependencies(self, path): return itertools.chain(super(GetAtt, self).dependencies(path), [self._resource(path)]) diff --git a/heat/engine/function.py b/heat/engine/function.py index 17438ec53..99b1caee0 100644 --- a/heat/engine/function.py +++ b/heat/engine/function.py @@ -59,6 +59,9 @@ class Function(object): def dependencies(self, path): return dependencies(self.args, '.'.join([path, self.fn_name])) + def dep_attrs(self, resource_name): + return dep_attrs(self.args, resource_name) + def __reduce__(self): """ Return a representation of the function suitable for pickling. @@ -167,3 +170,25 @@ def dependencies(snippet, path=''): else: return [] + + +def dep_attrs(snippet, resource_name): + """ + Return an iterator over dependent attributes for specified resource_name + in a template snippet. + + The snippet should be already parsed to insert Function objects where + appropriate. + """ + + if isinstance(snippet, Function): + return snippet.dep_attrs(resource_name) + + elif isinstance(snippet, collections.Mapping): + attrs = (dep_attrs(value, resource_name) for value in snippet.items()) + return itertools.chain.from_iterable(attrs) + elif (not isinstance(snippet, six.string_types) and + isinstance(snippet, collections.Iterable)): + attrs = (dep_attrs(value, resource_name) for value in snippet) + return itertools.chain.from_iterable(attrs) + return [] diff --git a/heat/engine/resource.py b/heat/engine/resource.py index f802fe94a..bbb44a024 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -419,6 +419,9 @@ class Resource(object): text = '%s "%s"' % (self.__class__.__name__, self.name) return encodeutils.safe_decode(text) + def dep_attrs(self, resource_name): + return self.t.dep_attrs(resource_name) + def add_dependencies(self, deps): for dep in self.t.dependencies(self.stack): deps += (self, dep) diff --git a/heat/engine/rsrc_defn.py b/heat/engine/rsrc_defn.py index 9cc989053..80df6d7aa 100644 --- a/heat/engine/rsrc_defn.py +++ b/heat/engine/rsrc_defn.py @@ -141,6 +141,16 @@ class ResourceDefinitionCore(object): deletion_policy=reparse_snippet(self._deletion_policy), update_policy=reparse_snippet(self._update_policy)) + def dep_attrs(self, resource_name): + """ + Return an iterator over dependent attributes for specified + resource_name in resources' properties and metadata fields. + """ + return itertools.chain(function.dep_attrs(self._properties, + resource_name), + function.dep_attrs(self._metadata, + resource_name)) + def dependencies(self, stack): """ Return the Resource objects in the given stack on which this depends. diff --git a/heat/engine/stack.py b/heat/engine/stack.py index e2bcfebd2..2d2caa53c 100755 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -14,6 +14,7 @@ import collections import copy import datetime +import itertools import re import warnings @@ -279,6 +280,19 @@ class Stack(collections.Mapping): if not self.parameters.set_stack_id(self.identifier()): LOG.warn(_LW("Unable to set parameters StackId identifier")) + @staticmethod + def get_dep_attrs(resources, outputs, resource_name): + ''' + Return the set of dependent attributes for specified resource name by + inspecting all resources and outputs in template. + ''' + attr_lists = itertools.chain((res.dep_attrs(resource_name) + for res in resources), + (function.dep_attrs(out.get('Value', ''), + resource_name) + for out in six.itervalues(outputs))) + return set(itertools.chain.from_iterable(attr_lists)) + @staticmethod def _get_dependencies(resources): '''Return the dependency graph for a list of resources.''' diff --git a/heat/tests/test_stack_collect_attributes.py b/heat/tests/test_stack_collect_attributes.py new file mode 100644 index 000000000..56faf19d0 --- /dev/null +++ b/heat/tests/test_stack_collect_attributes.py @@ -0,0 +1,182 @@ +# +# 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. + +from heat.common import template_format +from heat.engine import parser +from heat.engine import resource +from heat.engine import template +from heat.tests import common +from heat.tests import generic_resource as generic_rsrc +from heat.tests import utils + + +tmpl1 = """ +heat_template_version: 2014-10-16 +resources: + AResource: + type: ResourceWithPropsType + properties: + Foo: 'abc' +""" + +tmpl2 = """ +heat_template_version: 2014-10-16 +resources: + AResource: + type: ResourceWithPropsType + properties: + Foo: 'abc' + BResource: + type: ResourceWithPropsType + properties: + Foo: {get_attr: [AResource, attr_A1]} + metadata: + Foo: {get_attr: [AResource, attr_A1]} +outputs: + out1: + value: {get_attr: [AResource, attr_A1]} +""" + +tmpl3 = """ +heat_template_version: 2014-10-16 +resources: + AResource: + type: ResourceWithPropsType + properties: + Foo: 'abc' + BResource: + type: ResourceWithPropsType + properties: + Foo: {get_attr: [AResource, attr_A1]} + Doo: {get_attr: [AResource, attr_A2]} + Bar: {get_attr: [AResource, attr_A3]} + metadata: + first: {get_attr: [AResource, meta_A1]} + second: {get_attr: [AResource, meta_A2]} + third: {get_attr: [AResource, attr_A3]} +outputs: + out1: + value: {get_attr: [AResource, out_A1]} + out2: + value: {get_attr: [AResource, out_A2]} +""" + +tmpl4 = """ +heat_template_version: 2014-10-16 +resources: + AResource: + type: ResourceWithPropsType + properties: + Foo: 'abc' + BResource: + type: ResourceWithPropsType + properties: + Foo: 'abc' + CResource: + type: ResourceWithPropsType + properties: + Foo: 'abc' + DResource: + type: ResourceWithPropsType + properties: + Foo: {get_attr: [AResource, attr_A1]} + Doo: {get_attr: [BResource, attr_B1]} + metadata: + Doo: {get_attr: [CResource, attr_C1]} +outputs: + out1: + value: [{get_attr: [AResource, attr_A1]}, + {get_attr: [BResource, attr_B1]}, + {get_attr: [CResource, attr_C1]}] +""" + +tmpl5 = """ +heat_template_version: 2014-10-16 +resources: + AResource: + type: ResourceWithPropsType + properties: + Foo: 'abc' + BResource: + type: ResourceWithPropsType + properties: + Foo: {get_attr: [AResource, attr_A1]} + Doo: {get_attr: [AResource, attr_A2]} + metadata: + first: {get_attr: [AResource, meta_A1]} + CResource: + type: ResourceWithPropsType + properties: + Foo: {get_attr: [AResource, attr_A1]} + Doo: {get_attr: [BResource, attr_B2]} + metadata: + Doo: {get_attr: [BResource, attr_B1]} + first: {get_attr: [AResource, meta_A1]} + second: {get_attr: [BResource, meta_B2]} +outputs: + out1: + value: [{get_attr: [AResource, attr_A3]}, + {get_attr: [AResource, attr_A4]}, + {get_attr: [BResource, attr_B3]}] +""" + + +class DepAttrsTest(common.HeatTestCase): + + scenarios = [ + ('no_attr', + dict(tmpl=tmpl1, + expected={'AResource': set()})), + ('one_res_one_attr', + dict(tmpl=tmpl2, + expected={'AResource': {'attr_A1'}, + 'BResource': set()})), + ('one_res_several_attrs', + dict(tmpl=tmpl3, + expected={'AResource': {'attr_A1', 'attr_A2', 'attr_A3', + 'meta_A1', 'meta_A2', 'out_A1', + 'out_A2'}, + 'BResource': set()})), + ('several_res_one_attr', + dict(tmpl=tmpl4, + expected={'AResource': {'attr_A1'}, + 'BResource': {'attr_B1'}, + 'CResource': {'attr_C1'}, + 'DResource': set()})), + ('several_res_several_attrs', + dict(tmpl=tmpl5, + expected={'AResource': {'attr_A1', 'attr_A2', 'meta_A1', + 'attr_A3', 'attr_A4'}, + 'BResource': {'attr_B1', 'attr_B2', 'meta_B2', + 'attr_B3'}, + 'CResource': set()})) + ] + + def setUp(self): + super(DepAttrsTest, self).setUp() + self.ctx = utils.dummy_context() + resource._register_class('ResourceWithPropsType', + generic_rsrc.ResourceWithProps) + + def test_dep_attrs(self): + + parsed_tmpl = template_format.parse(self.tmpl) + self.stack = parser.Stack(self.ctx, 'test_stack', + template.Template(parsed_tmpl)) + resources = self.stack.resources.values() + outputs = self.stack.outputs + + for res in resources: + self.assertEqual(self.expected[res.name], + self.stack.get_dep_attrs(resources, outputs, + res.name))