From 1d7ba6e4a1de2bdfceffe5abf0efc95a5ef809db Mon Sep 17 00:00:00 2001 From: Anderson Mesquita Date: Tue, 1 Jul 2014 16:29:47 -0400 Subject: [PATCH] Add resource properties to stack-preview Certain resource details may influence the final price of the infrastructure being created by a stack (e.g. instance flavors), so this adds resources properties to the output of stack-preview. Implements: blueprint stack-preview-details Change-Id: I6ea892dedfc96aeda8e8a21e67562820fd21c7bb --- heat/engine/api.py | 18 +++++++- heat/engine/resources/template_resource.py | 10 ++++- heat/tests/test_engine_api_utils.py | 50 +++++++++++++++++++++- heat/tests/test_provider_template.py | 17 ++++++++ 4 files changed, 91 insertions(+), 4 deletions(-) diff --git a/heat/engine/api.py b/heat/engine/api.py index 66f7b3685..4e9222171 100644 --- a/heat/engine/api.py +++ b/heat/engine/api.py @@ -111,7 +111,18 @@ def format_stack(stack): return info -def format_stack_resource(resource, detail=True): +def format_resource_properties(resource): + def get_property(prop): + try: + return resource.properties[prop] + except (KeyError, ValueError): + return None + + return dict((prop, get_property(prop)) + for prop in resource.properties_schema.keys()) + + +def format_stack_resource(resource, detail=True, with_props=False): ''' Return a representation of the given resource that matches the API output expectations. @@ -142,6 +153,9 @@ def format_stack_resource(resource, detail=True): res[api.RES_DESCRIPTION] = resource.t.description res[api.RES_METADATA] = resource.metadata_get() + if with_props: + res[api.RES_SCHEMA_PROPERTIES] = format_resource_properties(resource) + return res @@ -149,7 +163,7 @@ def format_stack_preview(stack): def format_resource(res): if isinstance(res, list): return map(format_resource, res) - return format_stack_resource(res) + return format_stack_resource(res, with_props=True) fmt_stack = format_stack(stack) fmt_resources = map(format_resource, stack.preview_resources()) diff --git a/heat/engine/resources/template_resource.py b/heat/engine/resources/template_resource.py index 14f8df3a1..dc322ec40 100644 --- a/heat/engine/resources/template_resource.py +++ b/heat/engine/resources/template_resource.py @@ -108,7 +108,15 @@ class TemplateResource(stack_resource.StackResource): if not pval.implemented(): continue - val = self.properties[pname] + try: + val = self.properties[pname] + except ValueError: + if self.action == self.INIT: + prop = self.properties.props[pname] + val = prop.get_value(None) + else: + raise + if val is not None: # take a list and create a CommaDelimitedList if pval.type() == properties.Schema.LIST: diff --git a/heat/tests/test_engine_api_utils.py b/heat/tests/test_engine_api_utils.py index 2822f31bd..9b54a1708 100644 --- a/heat/tests/test_engine_api_utils.py +++ b/heat/tests/test_engine_api_utils.py @@ -46,6 +46,8 @@ class FormatTest(HeatTestCase): }) resource._register_class('GenericResourceType', generic_rsrc.GenericResource) + resource._register_class('ResWithComplexPropsAndAttrs', + generic_rsrc.ResWithComplexPropsAndAttrs) self.stack = parser.Stack(utils.dummy_context(), 'test_stack', template, stack_id=str(uuid.uuid4())) @@ -82,6 +84,49 @@ class FormatTest(HeatTestCase): formatted = api.format_stack_resource(res, False) self.assertEqual(resource_keys, set(formatted.keys())) + @mock.patch.object(api, 'format_resource_properties') + def test_format_stack_resource_with_props(self, mock_format_props): + mock_format_props.return_value = 'formatted_res_props' + res = self.stack['generic1'] + + formatted = api.format_stack_resource(res, True, with_props=True) + formatted_props = formatted[rpc_api.RES_SCHEMA_PROPERTIES] + self.assertEqual('formatted_res_props', formatted_props) + + def _get_formatted_resource_properties(self, res_name): + tmpl = parser.Template(template_format.parse(''' + heat_template_version: 2013-05-23 + resources: + resource1: + type: ResWithComplexPropsAndAttrs + resource2: + type: ResWithComplexPropsAndAttrs + properties: + a_string: foobar + resource3: + type: ResWithComplexPropsAndAttrs + properties: + a_string: { get_attr: [ resource2, string] } + ''')) + stack = parser.Stack(utils.dummy_context(), 'test_stack_for_preview', + tmpl, stack_id=str(uuid.uuid4())) + res = stack[res_name] + return api.format_resource_properties(res) + + def test_format_resource_properties_empty(self): + props = self._get_formatted_resource_properties('resource1') + self.assertIsNone(props['a_string']) + self.assertIsNone(props['a_list']) + self.assertIsNone(props['a_map']) + + def test_format_resource_properties_direct_props(self): + props = self._get_formatted_resource_properties('resource2') + self.assertEqual('foobar', props['a_string']) + + def test_format_resource_properties_get_attr(self): + props = self._get_formatted_resource_properties('resource3') + self.assertEqual('', props['a_string']) + def test_format_stack_resource_with_nested_stack(self): res = self.stack['generic1'] nested_id = {'foo': 'bar'} @@ -157,7 +202,7 @@ class FormatTest(HeatTestCase): @mock.patch.object(api, 'format_stack_resource') def test_format_stack_preview(self, mock_fmt_resource): - def mock_format_resources(res): + def mock_format_resources(res, **kwargs): return 'fmt%s' % res mock_fmt_resource.side_effect = mock_format_resources @@ -170,6 +215,9 @@ class FormatTest(HeatTestCase): self.assertIn('resources', stack) self.assertEqual(['fmt1', ['fmt2', ['fmt3']]], stack['resources']) + kwargs = mock_fmt_resource.call_args[1] + self.assertTrue(kwargs['with_props']) + def test_format_stack(self): self.stack.created_time = datetime(1970, 1, 1) info = api.format_stack(self.stack) diff --git a/heat/tests/test_provider_template.py b/heat/tests/test_provider_template.py index 6bf920757..05411719b 100644 --- a/heat/tests/test_provider_template.py +++ b/heat/tests/test_provider_template.py @@ -16,6 +16,8 @@ import os import uuid import yaml +import mock + from heat.common import exception from heat.common import template_format from heat.common import urlfetch @@ -157,6 +159,21 @@ class ProviderTemplateTest(HeatTestCase): # verify Map conversion self.assertEqual(map_prop_val, converted_params.get("AMap")) + with mock.patch.object(properties.Properties, '__getitem__') as m_get: + m_get.side_effect = ValueError('boom') + + # If the property doesn't exist on INIT, return default value + temp_res.action = temp_res.INIT + converted_params = temp_res.child_params() + for key in DummyResource.properties_schema: + self.assertIn(key, converted_params) + self.assertEqual({}, converted_params['AMap']) + self.assertEqual(0, converted_params['ANum']) + + # If the property doesn't exist past INIT, then error out + temp_res.action = temp_res.CREATE + self.assertRaises(ValueError, temp_res.child_params) + def test_attributes_extra(self): provider = { 'HeatTemplateFormatVersion': '2012-12-12',