Handle outputs with an OutputDefinition class

This is the last remaining area of the code where we were slinging around
CloudFormation-formatted snippets of template. Replace it with a proper API
along the lines of ResourceDefinition.

The default implementation of the new template.Template.outputs() method is
equivalent to what we have previously done spread throughout the code, so
that third-party Template plugins will not be affected until they have had
time to write their own custom implementations.

Change-Id: Ib65dad6db55ae5dafab473bebba67e841ca9a984
This commit is contained in:
Zane Bitter 2016-09-09 16:14:16 -04:00
parent e417fc3b86
commit 7de1c141db
15 changed files with 143 additions and 110 deletions

View File

@ -170,37 +170,33 @@ def translate_filters(params):
return params
def format_stack_outputs(stack, outputs, resolve_value=False):
def format_stack_outputs(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.
"""
return [format_stack_output(stack, outputs,
key, resolve_value=resolve_value)
return [format_stack_output(outputs[key], resolve_value=resolve_value)
for key in outputs]
def format_stack_output(stack, outputs, k, resolve_value=True):
def format_stack_output(output_defn, resolve_value=True):
result = {
rpc_api.OUTPUT_KEY: k,
rpc_api.OUTPUT_DESCRIPTION: outputs[k].get(stack.t.OUTPUT_DESCRIPTION,
'No description given'),
rpc_api.OUTPUT_KEY: output_defn.name,
rpc_api.OUTPUT_DESCRIPTION: output_defn.description(),
}
if resolve_value:
value = None
try:
value = stack.output(k)
value = output_defn.get_value()
except Exception as ex:
# We don't need error raising, just adding output_error to
# resulting dict.
value = None
result.update({rpc_api.OUTPUT_ERROR: six.text_type(ex)})
finally:
result.update({rpc_api.OUTPUT_VALUE: value})
if outputs[k].get('error_msg'):
result.update({rpc_api.OUTPUT_ERROR: outputs[k].get('error_msg')})
return result
@ -242,8 +238,7 @@ def format_stack(stack, preview=False, resolve_outputs=True):
# allow users to view the outputs of stacks
if stack.action != stack.DELETE and resolve_outputs:
info[rpc_api.STACK_OUTPUTS] = format_stack_outputs(stack,
stack.outputs,
info[rpc_api.STACK_OUTPUTS] = format_stack_outputs(stack.outputs,
resolve_value=True)
return info

51
heat/engine/output.py Normal file
View File

@ -0,0 +1,51 @@
#
# 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.
import six
from heat.engine import function
class OutputDefinition(object):
"""A definition of a stack output, independent of any template format."""
def __init__(self, name, value, description=None):
self.name = name
self._value = value
self._resolved_value = None
self._description = description
def validate(self):
"""Validate the output value without resolving it."""
function.validate(self._value)
def dep_attrs(self, resource_name):
"""Iterate over attributes of a given resource that this references.
Return an iterator over dependent attributes for specified
resource_name in the output's value field.
"""
return function.dep_attrs(self._value, resource_name)
def get_value(self):
"""Resolve the value of the output."""
if self._resolved_value is None:
self._resolved_value = function.resolve(self._value)
return self._resolved_value
def description(self):
"""Return a description of the output."""
if self._description is None:
return 'No description given'
return six.text_type(self._description)

View File

@ -585,14 +585,12 @@ class StackResource(resource.Resource):
stack = self.nested()
if stack is None:
return None
if op not in stack.outputs:
try:
return stack.outputs[op].get_value()
except (KeyError, Exception):
raise exception.InvalidTemplateAttribute(resource=self.name,
key=op)
result = stack.output(op)
if result is None and stack.outputs[op].get('error_msg') is not None:
raise exception.InvalidTemplateAttribute(resource=self.name,
key=op)
return result
def _resolve_attribute(self, name):
return self.get_output(name)

View File

@ -298,7 +298,7 @@ class TemplateResource(stack_resource.StackResource):
return six.text_type(self.name)
if 'OS::stack_id' in self.nested().outputs:
return self.nested().output('OS::stack_id')
return self.nested().outputs['OS::stack_id'].get_value()
return self.nested().identifier().arn()

View File

@ -1308,7 +1308,7 @@ class EngineService(service.Service):
s = self._get_stack(cntx, stack_identity)
stack = parser.Stack.load(cntx, stack=s)
return api.format_stack_outputs(stack, stack.t[stack.t.OUTPUTS])
return api.format_stack_outputs(stack.outputs)
@context.request_context
def show_output(self, cntx, stack_identity, output_key):
@ -1322,14 +1322,13 @@ class EngineService(service.Service):
s = self._get_stack(cntx, stack_identity)
stack = parser.Stack.load(cntx, stack=s)
outputs = stack.t[stack.t.OUTPUTS]
outputs = stack.outputs
if output_key not in outputs:
raise exception.NotFound(_('Specified output key %s not '
'found.') % output_key)
output = stack.resolve_outputs_data({output_key: outputs[output_key]})
return api.format_stack_output(stack, output, output_key)
return api.format_stack_output(outputs[output_key])
def _remote_call(self, cnxt, lock_engine_id, call, **kwargs):
timeout = cfg.CONF.engine_life_check_timeout

View File

@ -42,7 +42,6 @@ from heat.common import timeutils
from heat.engine import dependencies
from heat.engine import environment
from heat.engine import event
from heat.engine import function
from heat.engine.notification import stack as notification
from heat.engine import parameter_groups as param_groups
from heat.engine import resource
@ -297,8 +296,7 @@ class Stack(collections.Mapping):
@property
def outputs(self):
if self._outputs is None:
self._outputs = self.resolve_outputs_data(self.t[self.t.OUTPUTS],
path=self.t.OUTPUTS)
self._outputs = self.t.outputs(self)
return self._outputs
@property
@ -458,8 +456,7 @@ class Stack(collections.Mapping):
"""
attr_lists = itertools.chain((res.dep_attrs(resource_name)
for res in resources),
(function.dep_attrs(
out.get(value_sec, ''), resource_name)
(out.dep_attrs(resource_name)
for out in six.itervalues(outputs)))
return set(itertools.chain.from_iterable(attr_lists))
@ -834,32 +831,16 @@ class Stack(collections.Mapping):
if result:
raise exception.StackValidationFailed(message=result)
for key, val in self.outputs.items():
if not isinstance(val, collections.Mapping):
message = _('Outputs must contain Output. '
'Found a [%s] instead') % type(val)
raise exception.StackValidationFailed(
error='Output validation error',
path=[self.t.OUTPUTS],
message=message)
for op_name, output in six.iteritems(self.outputs):
try:
if not val or self.t.OUTPUT_VALUE not in val:
message = _('Each Output must contain '
'a Value key.')
raise exception.StackValidationFailed(
error='Output validation error',
path=[self.t.OUTPUTS, key],
message=message)
function.validate(val.get(self.t.OUTPUT_VALUE))
output.validate()
except exception.StackValidationFailed as ex:
raise
except AssertionError:
raise
except Exception as ex:
raise exception.StackValidationFailed(
error='Output validation error',
path=[self.t.OUTPUTS, key,
self.t.OUTPUT_VALUE],
error='Validation error in output "%s"' % op_name,
message=six.text_type(ex))
def requires_deferred_auth(self):
@ -1900,16 +1881,6 @@ class Stack(collections.Mapping):
action=self.RESTORE)
updater()
@profiler.trace('Stack.output', hide_args=False)
def output(self, key):
"""Get the value of the specified stack output."""
value = self.outputs[key].get(self.t.OUTPUT_VALUE, '')
try:
return function.resolve(value)
except Exception as ex:
self.outputs[key]['error_msg'] = six.text_type(ex)
return None
def restart_resource(self, resource_name):
"""Restart the resource specified by resource_name.
@ -1985,16 +1956,11 @@ class Stack(collections.Mapping):
def resolve_static_data(self, snippet, path=''):
warnings.warn('Stack.resolve_static_data() is deprecated and '
'will be removed in the Ocata release. Use the '
'Stack.resolve_outputs_data() instead.',
'will be removed in the Ocata release.',
DeprecationWarning)
return self.t.parse(self, snippet, path=path)
def resolve_outputs_data(self, outputs, path=''):
resolve_outputs = self.t.parse_outputs_conditions(outputs, self)
return self.t.parse(self, resolve_outputs, path=path)
def reset_resource_attributes(self):
# nothing is cached if no resources exist
if not self._resources:

View File

@ -25,6 +25,7 @@ from heat.common import exception
from heat.common.i18n import _
from heat.engine import environment
from heat.engine import function
from heat.engine import output
from heat.engine import template_files
from heat.objects import raw_template as template_object
@ -259,6 +260,36 @@ class Template(collections.Mapping):
"""Return a dictionary of resolved conditions."""
return {}
def outputs(self, stack):
resolve_outputs = self.parse_outputs_conditions(self[self.OUTPUTS],
stack)
outputs = self.parse(stack, resolve_outputs, path=self.OUTPUTS)
def get_outputs():
for key, val in outputs.items():
if not isinstance(val, collections.Mapping):
message = _('Outputs must contain Output. '
'Found a [%s] instead') % type(val)
raise exception.StackValidationFailed(
error='Output validation error',
path=[self.OUTPUTS, key],
message=message)
if self.OUTPUT_VALUE not in val:
message = _('Each output must contain '
'a %s key.') % self.OUTPUT_VALUE
raise exception.StackValidationFailed(
error='Output validation error',
path=[self.OUTPUTS, key],
message=message)
value_def = val[self.OUTPUT_VALUE]
description = val.get(self.OUTPUT_DESCRIPTION)
yield key, output.OutputDefinition(key, value_def, description)
return dict(get_outputs())
@abc.abstractmethod
def resource_definitions(self, stack):
"""Return a dictionary of ResourceDefinition objects."""

View File

@ -40,7 +40,7 @@ outputs:
self.assertEqual(self.rsrc.COMPLETE, self.rsrc.status)
self.assertEqual(self.stack.CREATE, self.stack.action)
self.assertEqual(self.stack.COMPLETE, self.stack.status)
self.assertIsNone(self.stack.output('anything'))
self.assertIsNone(self.stack.outputs['anything'].get_value())
def test_none_stack_create(self):
self._create_none_stack()

View File

@ -99,7 +99,7 @@ class TestValueSimple(TestValue):
stack = self.create_stack(templ_dict, env)
self.assertEqual(self.param1, stack['my_value'].FnGetAtt('value'))
self.assertEqual(self.param1, stack['my_value2'].FnGetAtt('value'))
self.assertEqual(self.param1, stack.output('myout'))
self.assertEqual(self.param1, stack.outputs['myout'].get_value())
class TestValueLessSimple(TestValue):

View File

@ -427,8 +427,7 @@ class FormatTest(common.HeatTestCase):
stack.status = 'COMPLETE'
stack['generic'].action = 'CREATE'
stack['generic'].status = 'COMPLETE'
info = api.format_stack_outputs(stack, stack.outputs,
resolve_value=True)
info = api.format_stack_outputs(stack.outputs, resolve_value=True)
expected = [{'description': 'No description given',
'output_error': 'The Referenced Attribute (generic Bar) '
'is incorrect.',
@ -463,7 +462,7 @@ 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.outputs)
expected = [{'description': 'No description given',
'output_key': 'incorrect_output'},
{'description': 'Good output',

View File

@ -1054,7 +1054,7 @@ class StackServiceTest(common.HeatTestCase):
self.patchobject(self.eng, '_get_stack')
self.patchobject(parser.Stack, 'load', return_value=stack)
self.patchobject(
stack, 'output',
stack.outputs['test'], 'get_value',
side_effect=[exception.EntityNotFound(entity='one', name='name')])
output = self.eng.show_output(self.ctx, mock.ANY, 'test')

View File

@ -741,7 +741,8 @@ class HOTemplateTest(common.HeatTestCase):
self.stack.create()
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
self.stack.state)
self.assertEqual('foo-success', self.stack.output('replaced'))
self.assertEqual('foo-success',
self.stack.outputs['replaced'].get_value())
def test_get_file(self):
"""Test get_file function."""

View File

@ -34,6 +34,7 @@ from heat.engine.clients.os import keystone
from heat.engine.clients.os import nova
from heat.engine import environment
from heat.engine import function
from heat.engine import output
from heat.engine import resource
from heat.engine import scheduler
from heat.engine import service
@ -989,7 +990,8 @@ class StackTest(common.HeatTestCase):
self.assertEqual('ADOPT', res.action)
self.assertEqual((self.stack.ADOPT, self.stack.COMPLETE),
self.stack.state)
self.assertEqual('AResource', self.stack.output('TestOutput'))
self.assertEqual('AResource',
self.stack.outputs['TestOutput'].get_value())
loaded_stack = stack.Stack.load(self.ctx, self.stack.id)
self.assertEqual({}, loaded_stack['AResource']._stored_properties_data)
@ -1292,13 +1294,16 @@ class StackTest(common.HeatTestCase):
(rsrc.UPDATE, rsrc.FAILED),
(rsrc.UPDATE, rsrc.COMPLETE)):
rsrc.state_set(action, status)
self.assertEqual('AResource', self.stack.output('TestOutput'))
self.stack._outputs = None
self.assertEqual('AResource',
self.stack.outputs['TestOutput'].get_value())
for action, status in (
(rsrc.DELETE, rsrc.IN_PROGRESS),
(rsrc.DELETE, rsrc.FAILED),
(rsrc.DELETE, rsrc.COMPLETE)):
rsrc.state_set(action, status)
self.assertIsNone(self.stack.output('TestOutput'))
self.stack._outputs = None
self.assertIsNone(self.stack.outputs['TestOutput'].get_value())
def test_resource_required_by(self):
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
@ -1704,7 +1709,8 @@ class StackTest(common.HeatTestCase):
self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
# According _resolve_attribute method in GenericResource output
# value will be equal with name AResource.
self.assertEqual('AResource', self.stack.output('Resource_attr'))
self.assertEqual('AResource',
self.stack.outputs['Resource_attr'].get_value())
self.stack.delete()
@ -1730,10 +1736,11 @@ class StackTest(common.HeatTestCase):
self.assertEqual((stack.Stack.CREATE, stack.Stack.COMPLETE),
self.stack.state)
self.assertIsNone(self.stack.output('Resource_attr'))
self.assertEqual('The Referenced Attribute (AResource Bar) is '
'incorrect.',
self.stack.outputs['Resource_attr']['error_msg'])
ex = self.assertRaises(exception.InvalidTemplateAttribute,
self.stack.outputs['Resource_attr'].get_value)
self.assertIn('The Referenced Attribute (AResource Bar) is '
'incorrect.',
six.text_type(ex))
self.stack.delete()
@ -1835,8 +1842,7 @@ class StackTest(common.HeatTestCase):
ex = self.assertRaises(exception.StackValidationFailed,
self.stack.validate)
self.assertEqual('Output validation error: '
'Outputs.Resource_attr.Value: '
self.assertEqual('Validation error in output "Resource_attr": '
'The Referenced Attribute '
'(AResource Bar) is incorrect.',
six.text_type(ex))
@ -1894,8 +1900,9 @@ class StackTest(common.HeatTestCase):
ex = self.assertRaises(exception.StackValidationFailed,
self.stack.validate)
self.assertIn('Each Output must contain a Value key.',
self.assertIn('Each output must contain a Value key.',
six.text_type(ex))
self.assertIn('Outputs.Resource_attr', six.text_type(ex))
def test_incorrect_outputs_cfn_empty_value(self):
tmpl = template_format.parse("""
@ -1952,6 +1959,7 @@ class StackTest(common.HeatTestCase):
self.assertIn('Outputs must contain Output. '
'Found a [%s] instead' % six.text_type,
six.text_type(ex))
self.assertIn('Outputs.Resource_attr', six.text_type(ex))
def test_prop_validate_value(self):
tmpl = template_format.parse("""
@ -2090,6 +2098,7 @@ class StackTest(common.HeatTestCase):
self.assertIn('Outputs must contain Output. '
'Found a [%s] instead' % type([]), six.text_type(ex))
self.assertIn('Outputs.Resource_attr', six.text_type(ex))
def test_incorrect_deletion_policy(self):
tmpl = template_format.parse("""
@ -2201,8 +2210,7 @@ class StackTest(common.HeatTestCase):
ex = self.assertRaises(exception.StackValidationFailed,
self.stack.validate)
self.assertEqual('Output validation error: '
'outputs.resource_attr.value: '
self.assertEqual('Validation error in output "resource_attr": '
'The Referenced Attribute '
'(AResource Bar) is incorrect.',
six.text_type(ex))
@ -2717,22 +2725,11 @@ class StackTest(common.HeatTestCase):
mock_dependency.validate.assert_called_once_with()
stc = stack.Stack(self.ctx, utils.random_name(), self.tmpl)
stc._outputs = {'foo': {'Value': 'bar'}}
stc._outputs = {'foo': output.OutputDefinition('foo', 'bar')}
func_val.side_effect = AssertionError(expected_msg)
expected_exception = self.assertRaises(AssertionError, stc.validate)
self.assertEqual(expected_msg, six.text_type(expected_exception))
def test_resolve_static_data_assertion_exception_rethrow(self):
tmpl = mock.MagicMock()
expected_message = 'Expected Assertion Error'
tmpl.parse.side_effect = AssertionError(expected_message)
stc = stack.Stack(self.ctx, utils.random_name(), tmpl)
expected_exception = self.assertRaises(AssertionError,
stc.resolve_outputs_data,
None)
self.assertEqual(expected_message, six.text_type(expected_exception))
@mock.patch.object(update, 'StackUpdate')
def test_update_task_exception(self, mock_stack_update):
class RandomException(Exception):

View File

@ -22,6 +22,7 @@ import six
from heat.common import exception
from heat.common import template_format
from heat.engine import output
from heat.engine import resource
from heat.engine.resources import stack_resource
from heat.engine import stack as parser
@ -624,8 +625,7 @@ class StackResourceAttrTest(StackResourceBaseTest):
nested = self.m.CreateMockAnything()
self.m.StubOutWithMock(stack_resource.StackResource, 'nested')
stack_resource.StackResource.nested().AndReturn(nested)
nested.outputs = {"key": "value"}
nested.output('key').AndReturn("value")
nested.outputs = {"key": output.OutputDefinition("key", "value")}
self.m.ReplayAll()
self.assertEqual("value", self.parent_resource.get_output("key"))
@ -649,8 +649,7 @@ class StackResourceAttrTest(StackResourceBaseTest):
nested = self.m.CreateMockAnything()
self.m.StubOutWithMock(stack_resource.StackResource, 'nested')
stack_resource.StackResource.nested().AndReturn(nested)
nested.outputs = {'key': 'value'}
nested.output('key').AndReturn('value')
nested.outputs = {'key': output.OutputDefinition('key', 'value')}
self.m.ReplayAll()
self.assertEqual('value',
@ -662,8 +661,8 @@ class StackResourceAttrTest(StackResourceBaseTest):
nested = self.m.CreateMockAnything()
self.m.StubOutWithMock(stack_resource.StackResource, 'nested')
stack_resource.StackResource.nested().AndReturn(nested)
nested.outputs = {'key': {'a': 1, 'b': 2}}
nested.output('key').AndReturn({'a': 1, 'b': 2})
nested.outputs = {'key': output.OutputDefinition('key',
{'a': 1, 'b': 2})}
self.m.ReplayAll()
self.assertEqual({'a': 1, 'b': 2},
@ -675,8 +674,7 @@ class StackResourceAttrTest(StackResourceBaseTest):
nested = self.m.CreateMockAnything()
self.m.StubOutWithMock(stack_resource.StackResource, 'nested')
stack_resource.StackResource.nested().AndReturn(nested)
nested.outputs = {"key": [1, 2, 3]}
nested.output('key').AndReturn([1, 2, 3])
nested.outputs = {'key': output.OutputDefinition('key', [1, 2, 3])}
self.m.ReplayAll()
self.assertEqual([1, 2, 3],

View File

@ -387,18 +387,16 @@ class TestTemplateConditionParser(common.HeatTestCase):
self.tmpl)
# test condition name is invalid
stk.outputs['foo']['condition'] = 'invalid_cd'
self.tmpl.t['outputs']['foo']['condition'] = 'invalid_cd'
ex = self.assertRaises(exception.InvalidConditionReference,
self.tmpl.parse_outputs_conditions,
stk.outputs, stk)
lambda: stk.outputs)
self.assertIn('Invalid condition "invalid_cd" '
'(in outputs.foo.condition)',
six.text_type(ex))
# test condition name is not string
stk.outputs['foo']['condition'] = 222
self.tmpl.t['outputs']['foo']['condition'] = 222
ex = self.assertRaises(exception.InvalidConditionReference,
self.tmpl.parse_outputs_conditions,
stk.outputs, stk)
lambda: stk.outputs)
self.assertIn('Invalid condition "222" (in outputs.foo.condition)',
six.text_type(ex))