diff --git a/heat/engine/api.py b/heat/engine/api.py index 6d3476c4b6..965ada9584 100644 --- a/heat/engine/api.py +++ b/heat/engine/api.py @@ -170,24 +170,30 @@ def translate_filters(params): return params -def format_stack_outputs(stack, outputs): +def format_stack_outputs(stack, outputs, resolve_value=False): """Return a representation of the given output template. Return a representation of the given output template for the given stack that matches the API output expectations. """ - def format_stack_output(k): - output = { - rpc_api.OUTPUT_DESCRIPTION: outputs[k].get('Description', - 'No description given'), - rpc_api.OUTPUT_KEY: k, - rpc_api.OUTPUT_VALUE: stack.output(k) - } - if outputs[k].get('error_msg'): - output.update({rpc_api.OUTPUT_ERROR: outputs[k].get('error_msg')}) - return output + return [format_stack_output(stack, outputs, + key, resolve_value=resolve_value) + for key in outputs] - return [format_stack_output(key) for key in outputs] + +def format_stack_output(stack, outputs, k, resolve_value=True): + result = { + rpc_api.OUTPUT_KEY: k, + rpc_api.OUTPUT_DESCRIPTION: outputs[k].get('Description', + 'No description given'), + } + + if resolve_value: + result.update({rpc_api.OUTPUT_VALUE: stack.output(k)}) + + if outputs[k].get('error_msg'): + result.update({rpc_api.OUTPUT_ERROR: outputs[k].get('error_msg')}) + return result def format_stack(stack, preview=False): @@ -227,7 +233,8 @@ def format_stack(stack, preview=False): # allow users to view the outputs of stacks if stack.action != stack.DELETE and stack.status != stack.IN_PROGRESS: info[rpc_api.STACK_OUTPUTS] = format_stack_outputs(stack, - stack.outputs) + stack.outputs, + resolve_value=True) return info diff --git a/heat/engine/service.py b/heat/engine/service.py index 26e01cb71f..449f439c27 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -1069,6 +1069,39 @@ class EngineService(service.Service): return s.raw_template.template return None + @context.request_context + def list_outputs(self, cntx, stack_identity): + """Get a list of stack outputs. + + :param cntx: RPC context. + :param stack_identity: Name of the stack you want to see. + :return: list of stack outputs in defined format. + """ + s = self._get_stack(cntx, stack_identity) + stack = parser.Stack.load(cntx, stack=s, resolve_data=False) + + return api.format_stack_outputs(stack, stack.t[stack.t.OUTPUTS]) + + @context.request_context + def show_output(self, cntx, stack_identity, output_key): + """Returns dict with specified output key, value and description. + + :param cntx: RPC context. + :param stack_identity: Name of the stack you want to see. + :param output_key: key of desired stack output. + :return: dict with output key, value and description in defined format. + """ + s = self._get_stack(cntx, stack_identity) + stack = parser.Stack.load(cntx, stack=s, resolve_data=False) + + outputs = stack.t[stack.t.OUTPUTS] + + if output_key not in outputs: + raise exception.NotFound(_('Specified output key %s not ' + 'found.') % output_key) + output = stack.resolve_static_data(outputs[output_key]) + return api.format_stack_output(stack, {output_key: output}, output_key) + def _remote_call(self, cnxt, lock_engine_id, call, **kwargs): timeout = cfg.CONF.engine_life_check_timeout self.cctxt = self._client.prepare( diff --git a/heat/engine/stack.py b/heat/engine/stack.py index b8bdedc3fc..638289cb52 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -382,7 +382,8 @@ class Stack(collections.Mapping): @classmethod def load(cls, context, stack_id=None, stack=None, show_deleted=True, - use_stored_context=False, force_reload=False, cache_data=None): + use_stored_context=False, force_reload=False, cache_data=None, + resolve_data=True): """Retrieve a Stack from the database.""" if stack is None: stack = stack_object.Stack.get_by_id( @@ -399,7 +400,7 @@ class Stack(collections.Mapping): return cls._from_db(context, stack, use_stored_context=use_stored_context, - cache_data=cache_data) + cache_data=cache_data, resolve_data=resolve_data) @classmethod def load_all(cls, context, limit=None, marker=None, sort_keys=None, diff --git a/heat/tests/test_engine_api_utils.py b/heat/tests/test_engine_api_utils.py index 826f204b74..39546461e8 100644 --- a/heat/tests/test_engine_api_utils.py +++ b/heat/tests/test_engine_api_utils.py @@ -412,7 +412,8 @@ class FormatTest(common.HeatTestCase): stack.status = 'COMPLETE' stack['generic'].action = 'CREATE' stack['generic'].status = 'COMPLETE' - info = api.format_stack_outputs(stack, stack.outputs) + info = api.format_stack_outputs(stack, stack.outputs, + resolve_value=True) expected = [{'description': 'No description given', 'output_error': 'The Referenced Attribute (generic Bar) ' 'is incorrect.', @@ -425,6 +426,37 @@ class FormatTest(common.HeatTestCase): self.assertEqual(expected, sorted(info, key=lambda k: k['output_key'], reverse=True)) + def test_format_stack_outputs_unresolved(self): + tmpl = template.Template({ + 'HeatTemplateFormatVersion': '2012-12-12', + 'Resources': { + 'generic': {'Type': 'GenericResourceType'} + }, + 'Outputs': { + 'correct_output': { + 'Description': 'Good output', + 'Value': {'Fn::GetAtt': ['generic', 'Foo']} + }, + 'incorrect_output': { + 'Value': {'Fn::GetAtt': ['generic', 'Bar']} + } + } + }) + stack = parser.Stack(utils.dummy_context(), 'test_stack', + tmpl, stack_id=str(uuid.uuid4())) + stack.action = 'CREATE' + stack.status = 'COMPLETE' + stack['generic'].action = 'CREATE' + stack['generic'].status = 'COMPLETE' + info = api.format_stack_outputs(stack, stack.outputs) + expected = [{'description': 'No description given', + 'output_key': 'incorrect_output'}, + {'description': 'Good output', + 'output_key': 'correct_output'}] + + self.assertEqual(expected, sorted(info, key=lambda k: k['output_key'], + reverse=True)) + class FormatValidateParameterTest(common.HeatTestCase): diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index 4e9b38188c..01aaf9d34b 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -1019,6 +1019,62 @@ class StackServiceTest(common.HeatTestCase): msg = "Template with version %s not found" % version self.assertEqual(msg, six.text_type(ex)) + def test_stack_list_outputs(self): + t = template_format.parse(tools.wp_template) + t['outputs'] = { + 'test': {'value': '{ get_attr: fir }', + 'description': 'sec'}, + 'test2': {'value': 'sec'}} + tmpl = templatem.Template(t) + stack = parser.Stack(self.ctx, 'service_list_outputs_stack', tmpl) + + self.patchobject(self.eng, '_get_stack') + self.patchobject(parser.Stack, 'load', return_value=stack) + + outputs = self.eng.list_outputs(self.ctx, mock.ANY) + + self.assertIn({'output_key': 'test', + 'description': 'sec'}, outputs) + self.assertIn({'output_key': 'test2', + 'description': 'No description given'}, + outputs) + + def test_stack_empty_list_outputs(self): + # Ensure that stack with no output returns empty list + t = template_format.parse(tools.wp_template) + t['outputs'] = {} + tmpl = templatem.Template(t) + stack = parser.Stack(self.ctx, 'service_list_outputs_stack', tmpl) + + self.patchobject(self.eng, '_get_stack') + self.patchobject(parser.Stack, 'load', return_value=stack) + + outputs = self.eng.list_outputs(self.ctx, mock.ANY) + self.assertEqual([], outputs) + + def test_stack_show_output(self): + t = template_format.parse(tools.wp_template) + t['outputs'] = {'test': {'value': 'first', 'description': 'sec'}, + 'test2': {'value': 'sec'}} + tmpl = templatem.Template(t) + stack = parser.Stack(self.ctx, 'service_list_outputs_stack', tmpl) + + self.patchobject(self.eng, '_get_stack') + self.patchobject(parser.Stack, 'load', return_value=stack) + + output = self.eng.show_output(self.ctx, mock.ANY, 'test') + self.assertEqual({'output_key': 'test', 'output_value': 'first', + 'description': 'sec'}, + output) + + # Ensure that stack raised NotFound error with incorrect key. + ex = self.assertRaises(dispatcher.ExpectedException, + self.eng.show_output, + self.ctx, mock.ANY, 'bunny') + self.assertEqual(exception.NotFound, ex.exc_info[0]) + self.assertEqual('Specified output key bunny not found.', + six.text_type(ex.exc_info[1])) + def test_stack_list_all_empty(self): sl = self.eng.list_stacks(self.ctx)