deb-heat/heat/tests/test_hot.py
Sergey Kraynev 752c0112ad Handle error in templates with incorrect resources
This patch adds additional validation resources section for hot templates,
it allows determine of the real problem in template and give the user an
adequate message.

Change-Id: Iff5e1b541b00a82dfce524f00ff1d20f50ae92da
Closes-Bug: #1365949
2014-10-10 03:11:50 -04:00

1478 lines
54 KiB
Python

#
# 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 copy
import six
from heat.common import exception
from heat.common import identifier
from heat.common import template_format
from heat.engine import environment
from heat.engine import function
from heat.engine.hot import functions as hot_functions
from heat.engine.hot import parameters as hot_param
from heat.engine.hot import template as hot_template
from heat.engine import parameters
from heat.engine import parser
from heat.engine import resource
from heat.engine import resources
from heat.engine import rsrc_defn
from heat.engine import template
from heat.tests.common import HeatTestCase
from heat.tests import generic_resource as generic_rsrc
from heat.tests import test_parser
from heat.tests import utils
hot_tpl_empty = template_format.parse('''
heat_template_version: 2013-05-23
''')
hot_juno_tpl_empty = template_format.parse('''
heat_template_version: 2014-10-16
''')
hot_tpl_empty_sections = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
resources:
outputs:
''')
hot_tpl_generic_resource = template_format.parse('''
heat_template_version: 2013-05-23
resources:
resource1:
type: GenericResourceType
''')
hot_tpl_complex_attrs = template_format.parse('''
heat_template_version: 2013-05-23
resources:
resource1:
type: ResourceWithComplexAttributesType
''')
hot_tpl_mapped_props = template_format.parse('''
heat_template_version: 2013-05-23
resources:
resource1:
type: ResWithComplexPropsAndAttrsType
resource2:
type: ResWithComplexPropsAndAttrsType
properties:
a_list: { get_attr: [ resource1, list] }
a_string: { get_attr: [ resource1, string ] }
a_map: { get_attr: [ resource1, map] }
''')
class DummyClass(object):
metadata = None
def metadata_get(self):
return self.metadata
def metadata_set(self, metadata):
self.metadata = metadata
class HOTemplateTest(HeatTestCase):
"""Test processing of HOT templates."""
@staticmethod
def resolve(snippet, template, stack=None):
return function.resolve(template.parse(stack, snippet))
def test_defaults(self):
"""Test default content behavior of HOT template."""
tmpl = parser.Template(hot_tpl_empty)
# check if we get the right class
self.assertIsInstance(tmpl, hot_template.HOTemplate20130523)
# test getting an invalid section
self.assertNotIn('foobar', tmpl)
# test defaults for valid sections
self.assertEqual('No description', tmpl[tmpl.DESCRIPTION])
self.assertEqual({}, tmpl[tmpl.RESOURCES])
self.assertEqual({}, tmpl[tmpl.OUTPUTS])
def test_defaults_for_empty_sections(self):
"""Test default secntion's content behavior of HOT template."""
tmpl = parser.Template(hot_tpl_empty_sections)
# check if we get the right class
self.assertIsInstance(tmpl, hot_template.HOTemplate20130523)
# test getting an invalid section
self.assertNotIn('foobar', tmpl)
# test defaults for valid sections
self.assertEqual('No description', tmpl[tmpl.DESCRIPTION])
self.assertEqual({}, tmpl[tmpl.RESOURCES])
self.assertEqual({}, tmpl[tmpl.OUTPUTS])
stack = parser.Stack(utils.dummy_context(), 'test_stack', tmpl)
self.assertIsNone(stack.parameters._validate_user_parameters())
self.assertIsNone(stack.parameters._validate_tmpl_parameters())
self.assertIsNone(stack.validate())
def test_translate_resources_good(self):
"""Test translation of resources into internal engine format."""
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
resources:
resource1:
type: AWS::EC2::Instance
properties:
property1: value1
metadata:
foo: bar
depends_on: dummy
deletion_policy: dummy
update_policy:
foo: bar
''')
expected = {'resource1': {'Type': 'AWS::EC2::Instance',
'Properties': {'property1': 'value1'},
'Metadata': {'foo': 'bar'},
'DependsOn': 'dummy',
'DeletionPolicy': 'dummy',
'UpdatePolicy': {'foo': 'bar'}}}
tmpl = parser.Template(hot_tpl)
self.assertEqual(expected, tmpl[tmpl.RESOURCES])
def test_translate_resources_bad_type(self):
"""Test translation of resources including invalid keyword."""
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
resources:
resource1:
Type: AWS::EC2::Instance
properties:
property1: value1
metadata:
foo: bar
depends_on: dummy
deletion_policy: dummy
update_policy:
foo: bar
''')
tmpl = parser.Template(hot_tpl)
err = self.assertRaises(KeyError, tmpl.__getitem__, tmpl.RESOURCES)
self.assertEqual('u\'"Type" is not a valid keyword '
'inside a resource definition\'',
six.text_type(err))
def test_translate_resources_bad_properties(self):
"""Test translation of resources including invalid keyword."""
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
resources:
resource1:
type: AWS::EC2::Instance
Properties:
property1: value1
metadata:
foo: bar
depends_on: dummy
deletion_policy: dummy
update_policy:
foo: bar
''')
tmpl = parser.Template(hot_tpl)
err = self.assertRaises(KeyError, tmpl.__getitem__, tmpl.RESOURCES)
self.assertEqual('u\'"Properties" is not a valid keyword '
'inside a resource definition\'',
six.text_type(err))
def test_translate_resources_resources_without_name(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
resources:
type: AWS::EC2::Instance
properties:
property1: value1
metadata:
foo: bar
depends_on: dummy
deletion_policy: dummy
''')
tmpl = parser.Template(hot_tpl)
error = self.assertRaises(exception.StackValidationFailed,
tmpl.__getitem__, tmpl.RESOURCES)
self.assertEqual('"resources" must contain a map of resource maps.',
six.text_type(error))
def test_translate_resources_bad_metadata(self):
"""Test translation of resources including invalid keyword."""
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
resources:
resource1:
type: AWS::EC2::Instance
properties:
property1: value1
Metadata:
foo: bar
depends_on: dummy
deletion_policy: dummy
update_policy:
foo: bar
''')
tmpl = parser.Template(hot_tpl)
err = self.assertRaises(KeyError, tmpl.__getitem__, tmpl.RESOURCES)
self.assertEqual('u\'"Metadata" is not a valid keyword '
'inside a resource definition\'',
six.text_type(err))
def test_translate_resources_bad_depends_on(self):
"""Test translation of resources including invalid keyword."""
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
resources:
resource1:
type: AWS::EC2::Instance
properties:
property1: value1
metadata:
foo: bar
DependsOn: dummy
deletion_policy: dummy
update_policy:
foo: bar
''')
tmpl = parser.Template(hot_tpl)
err = self.assertRaises(KeyError, tmpl.__getitem__, tmpl.RESOURCES)
self.assertEqual('u\'"DependsOn" is not a valid keyword '
'inside a resource definition\'',
six.text_type(err))
def test_translate_resources_bad_deletion_polciy(self):
"""Test translation of resources including invalid keyword."""
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
resources:
resource1:
type: AWS::EC2::Instance
properties:
property1: value1
metadata:
foo: bar
depends_on: dummy
DeletionPolicy: dummy
update_policy:
foo: bar
''')
tmpl = parser.Template(hot_tpl)
err = self.assertRaises(KeyError, tmpl.__getitem__, tmpl.RESOURCES)
self.assertEqual('u\'"DeletionPolicy" is not a valid keyword '
'inside a resource definition\'',
six.text_type(err))
def test_translate_resources_bad_update_policy(self):
"""Test translation of resources including invalid keyword."""
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
resources:
resource1:
type: AWS::EC2::Instance
properties:
property1: value1
metadata:
foo: bar
depends_on: dummy
deletion_policy: dummy
UpdatePolicy:
foo: bar
''')
tmpl = parser.Template(hot_tpl)
err = self.assertRaises(KeyError, tmpl.__getitem__, tmpl.RESOURCES)
self.assertEqual('u\'"UpdatePolicy" is not a valid keyword '
'inside a resource definition\'',
six.text_type(err))
def test_translate_outputs_good(self):
"""Test translation of outputs into internal engine format."""
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
outputs:
output1:
description: output1
value: value1
''')
expected = {'output1': {'Description': 'output1', 'Value': 'value1'}}
tmpl = parser.Template(hot_tpl)
self.assertEqual(expected, tmpl[tmpl.OUTPUTS])
def test_translate_outputs_bad_description(self):
"""Test translation of outputs into internal engine format."""
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
outputs:
output1:
Description: output1
value: value1
''')
tmpl = parser.Template(hot_tpl)
err = self.assertRaises(KeyError, tmpl.__getitem__, tmpl.OUTPUTS)
self.assertIn('Description', six.text_type(err))
def test_translate_outputs_bad_value(self):
"""Test translation of outputs into internal engine format."""
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
outputs:
output1:
description: output1
Value: value1
''')
tmpl = parser.Template(hot_tpl)
err = self.assertRaises(KeyError, tmpl.__getitem__, tmpl.OUTPUTS)
self.assertIn('Value', six.text_type(err))
def test_resource_group_list_join(self):
"""Test list_join on a ResourceGroup's inner attributes
This should not fail during validation (i.e. before the ResourceGroup
can return the list of the runtime values.
"""
hot_tpl = template_format.parse('''
heat_template_version: 2014-10-16
resources:
rg:
type: OS::Heat::ResourceGroup
properties:
count: 3
resource_def:
type: OS::Nova::Server
''')
tmpl = parser.Template(hot_tpl)
stack = parser.Stack(utils.dummy_context(), 'test_stack', tmpl)
snippet = {'list_join': ["\n", {'get_attr': ['rg', 'name']}]}
self.assertEqual(self.resolve(snippet, tmpl, stack), '')
def test_str_replace(self):
"""Test str_replace function."""
snippet = {'str_replace': {'template': 'Template var1 string var2',
'params': {'var1': 'foo', 'var2': 'bar'}}}
snippet_resolved = 'Template foo string bar'
tmpl = parser.Template(hot_tpl_empty)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_str_replace_number(self):
"""Test str_replace function with numbers."""
snippet = {'str_replace': {'template': 'Template number string bar',
'params': {'number': 1}}}
snippet_resolved = 'Template 1 string bar'
tmpl = parser.Template(hot_tpl_empty)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_str_fn_replace(self):
"""Test Fn:Replace function."""
snippet = {'Fn::Replace': [{'$var1': 'foo', '$var2': 'bar'},
'Template $var1 string $var2']}
snippet_resolved = 'Template foo string bar'
tmpl = parser.Template(hot_tpl_empty)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_str_replace_syntax(self):
"""
Test str_replace function syntax.
Pass wrong syntax (array instead of dictionary) to function and
validate that we get a TypeError.
"""
snippet = {'str_replace': [{'template': 'Template var1 string var2'},
{'params': {'var1': 'foo', 'var2': 'bar'}}]}
tmpl = parser.Template(hot_tpl_empty)
self.assertRaises(TypeError, self.resolve, snippet, tmpl)
def test_str_replace_invalid_param_keys(self):
"""
Test str_replace function parameter keys.
Pass wrong parameters to function and verify that we get
a KeyError.
"""
snippet = {'str_replace': {'tmpl': 'Template var1 string var2',
'params': {'var1': 'foo', 'var2': 'bar'}}}
tmpl = parser.Template(hot_tpl_empty)
self.assertRaises(KeyError, self.resolve, snippet, tmpl)
snippet = {'str_replace': {'tmpl': 'Template var1 string var2',
'parms': {'var1': 'foo', 'var2': 'bar'}}}
self.assertRaises(KeyError, self.resolve, snippet, tmpl)
def test_str_replace_invalid_param_types(self):
"""
Test str_replace function parameter values.
Pass parameter values of wrong type to function and verify that we get
a TypeError.
"""
snippet = {'str_replace': {'template': 12345,
'params': {'var1': 'foo', 'var2': 'bar'}}}
tmpl = parser.Template(hot_tpl_empty)
self.assertRaises(TypeError, self.resolve, snippet, tmpl)
snippet = {'str_replace': {'template': 'Template var1 string var2',
'params': ['var1', 'foo', 'var2', 'bar']}}
self.assertRaises(TypeError, self.resolve, snippet, tmpl)
def test_get_file(self):
"""Test get_file function."""
snippet = {'get_file': 'file:///tmp/foo.yaml'}
snippet_resolved = 'foo contents'
tmpl = parser.Template(hot_tpl_empty, files={
'file:///tmp/foo.yaml': 'foo contents'
})
stack = parser.Stack(utils.dummy_context(), 'param_id_test', tmpl)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl, stack))
def test_get_file_not_string(self):
"""Test get_file function with non-string argument."""
snippet = {'get_file': ['file:///tmp/foo.yaml']}
tmpl = parser.Template(hot_tpl_empty)
stack = parser.Stack(utils.dummy_context(), 'param_id_test', tmpl)
notStrErr = self.assertRaises(TypeError, self.resolve,
snippet, tmpl, stack)
self.assertEqual(
'Argument to "get_file" must be a string',
six.text_type(notStrErr))
def test_get_file_missing_files(self):
"""Test get_file function with no matching key in files section."""
snippet = {'get_file': 'file:///tmp/foo.yaml'}
tmpl = parser.Template(hot_tpl_empty, files={
'file:///tmp/bar.yaml': 'bar contents'
})
stack = parser.Stack(utils.dummy_context(), 'param_id_test', tmpl)
missingErr = self.assertRaises(ValueError, self.resolve,
snippet, tmpl, stack)
self.assertEqual(
('No content found in the "files" section for '
'get_file path: file:///tmp/foo.yaml'),
six.text_type(missingErr))
def test_get_file_nested_does_not_resolve(self):
"""Test get_file function does not resolve nested calls."""
snippet = {'get_file': 'file:///tmp/foo.yaml'}
snippet_resolved = '{get_file: file:///tmp/bar.yaml}'
tmpl = parser.Template(hot_tpl_empty, files={
'file:///tmp/foo.yaml': snippet_resolved,
'file:///tmp/bar.yaml': 'bar content',
})
stack = parser.Stack(utils.dummy_context(), 'param_id_test', tmpl)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl, stack))
def test_prevent_parameters_access(self):
"""
Test that the parameters section can't be accessed using the template
as a dictionary.
"""
expected_description = "This can be accessed"
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
description: {0}
parameters:
foo:
type: string
'''.format(expected_description))
tmpl = parser.Template(hot_tpl)
self.assertEqual(expected_description, tmpl['description'])
err_str = "can not be accessed directly"
#Hot template test
keyError = self.assertRaises(KeyError, tmpl.__getitem__, 'parameters')
self.assertIn(err_str, six.text_type(keyError))
#CFN template test
keyError = self.assertRaises(KeyError, tmpl.__getitem__, 'Parameters')
self.assertIn(err_str, six.text_type(keyError))
def test_parameters_section_not_iterable(self):
"""
Test that the parameters section is not returned when the template is
used as an iterable.
"""
expected_description = "This can be accessed"
tmpl = parser.Template({'heat_template_version': '2013-05-23',
'description': expected_description,
'parameters':
{'foo': {'Type': 'String', 'Required': True}}})
self.assertEqual(expected_description, tmpl['description'])
self.assertNotIn('parameters', tmpl.keys())
def test_invalid_hot_version(self):
"""
Test HOT version check.
Pass an invalid HOT version to template.Template.__new__() and
validate that we get a ValueError.
"""
tmpl_str = "heat_template_version: this-ain't-valid"
hot_tmpl = template_format.parse(tmpl_str)
self.assertRaises(exception.InvalidTemplateVersion,
template.Template, hot_tmpl)
def test_valid_hot_version(self):
"""
Test HOT version check.
Pass a valid HOT version to template.Template.__new__() and
validate that we get back a parsed template.
"""
tmpl_str = "heat_template_version: 2013-05-23"
hot_tmpl = template_format.parse(tmpl_str)
parsed_tmpl = template.Template(hot_tmpl)
expected = ('heat_template_version', '2013-05-23')
observed = parsed_tmpl.version
self.assertEqual(expected, observed)
def test_resource_facade(self):
metadata_snippet = {'resource_facade': 'metadata'}
deletion_policy_snippet = {'resource_facade': 'deletion_policy'}
update_policy_snippet = {'resource_facade': 'update_policy'}
parent_resource = DummyClass()
parent_resource.metadata_set({"foo": "bar"})
parent_resource.t = rsrc_defn.ResourceDefinition(
'parent', 'SomeType',
deletion_policy=rsrc_defn.ResourceDefinition.RETAIN,
update_policy={"blarg": "wibble"})
parent_resource.stack = parser.Stack(utils.dummy_context(),
'toplevel_stack',
parser.Template(hot_tpl_empty))
stack = parser.Stack(utils.dummy_context(), 'test_stack',
parser.Template(hot_tpl_empty),
parent_resource=parent_resource)
self.assertEqual({"foo": "bar"},
self.resolve(metadata_snippet, stack.t, stack))
self.assertEqual('Retain',
self.resolve(deletion_policy_snippet, stack.t, stack))
self.assertEqual({"blarg": "wibble"},
self.resolve(update_policy_snippet, stack.t, stack))
def test_resource_facade_function(self):
deletion_policy_snippet = {'resource_facade': 'deletion_policy'}
parent_resource = DummyClass()
parent_resource.metadata_set({"foo": "bar"})
tmpl = parser.Template(hot_juno_tpl_empty)
parent_resource.stack = parser.Stack(utils.dummy_context(),
'toplevel_stack',
tmpl)
del_policy = hot_functions.Join(parent_resource.stack,
'list_join', ['eta', ['R', 'in']])
parent_resource.t = rsrc_defn.ResourceDefinition(
'parent', 'SomeType',
deletion_policy=del_policy)
stack = parser.Stack(utils.dummy_context(), 'test_stack',
parser.Template(hot_tpl_empty),
parent_resource=parent_resource)
self.assertEqual('Retain',
self.resolve(deletion_policy_snippet, stack.t, stack))
def test_resource_facade_invalid_arg(self):
snippet = {'resource_facade': 'wibble'}
stack = parser.Stack(utils.dummy_context(), 'test_stack',
parser.Template(hot_tpl_empty))
error = self.assertRaises(ValueError,
self.resolve,
snippet,
stack.t, stack)
self.assertIn(snippet.keys()[0], six.text_type(error))
def test_resource_facade_missing_deletion_policy(self):
snippet = {'resource_facade': 'deletion_policy'}
parent_resource = DummyClass()
parent_resource.metadata_set({"foo": "bar"})
parent_resource.t = rsrc_defn.ResourceDefinition('parent', 'SomeType')
parent_resource.stack = parser.Stack(utils.dummy_context(),
'toplevel_stack',
parser.Template(hot_tpl_empty))
stack = parser.Stack(utils.dummy_context(), 'test_stack',
parser.Template(hot_tpl_empty),
parent_resource=parent_resource)
self.assertEqual('Delete', self.resolve(snippet, stack.t, stack))
def test_removed_function(self):
snippet = {'Fn::GetAZs': ''}
stack = parser.Stack(utils.dummy_context(), 'test_stack',
parser.Template(hot_juno_tpl_empty))
error = self.assertRaises(exception.InvalidTemplateVersion,
function.validate,
stack.t.parse(stack, snippet))
self.assertIn(snippet.keys()[0], six.text_type(error))
def test_add_resource(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
resources:
resource1:
type: AWS::EC2::Instance
properties:
property1: value1
metadata:
foo: bar
depends_on:
- dummy
deletion_policy: Retain
update_policy:
foo: bar
''')
source = parser.Template(hot_tpl)
empty = parser.Template(copy.deepcopy(hot_tpl_empty))
stack = parser.Stack(utils.dummy_context(), 'test_stack', source)
for defn in source.resource_definitions(stack).values():
empty.add_resource(defn)
self.assertEqual(hot_tpl['resources'], empty.t['resources'])
class StackTest(test_parser.StackTest):
"""Test stack function when stack was created from HOT template."""
def resolve(self, snippet):
return function.resolve(self.stack.t.parse(self.stack, snippet))
def test_get_attr_multiple_rsrc_status(self):
"""Test resolution of get_attr occurrences in HOT template."""
hot_tpl = hot_tpl_generic_resource
self.stack = parser.Stack(self.ctx, 'test_get_attr',
template.Template(hot_tpl))
self.stack.store()
self.stack.create()
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
self.stack.state)
snippet = {'Value': {'get_attr': ['resource1', 'foo']}}
rsrc = self.stack['resource1']
for action, status in (
(rsrc.CREATE, rsrc.IN_PROGRESS),
(rsrc.CREATE, rsrc.COMPLETE),
(rsrc.RESUME, rsrc.IN_PROGRESS),
(rsrc.RESUME, rsrc.COMPLETE),
(rsrc.UPDATE, rsrc.IN_PROGRESS),
(rsrc.UPDATE, rsrc.COMPLETE)):
rsrc.state_set(action, status)
# GenericResourceType has an attribute 'foo' which yields the
# resource name.
self.assertEqual({'Value': 'resource1'}, self.resolve(snippet))
def test_get_attr_invalid(self):
"""Test resolution of get_attr occurrences in HOT template."""
hot_tpl = hot_tpl_generic_resource
self.stack = parser.Stack(self.ctx, 'test_get_attr',
template.Template(hot_tpl))
self.stack.store()
self.stack.create()
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
self.stack.state)
self.assertRaises(exception.InvalidTemplateAttribute,
self.resolve,
{'Value': {'get_attr': ['resource1', 'NotThere']}})
def test_get_attr_invalid_resource(self):
"""Test resolution of get_attr occurrences in HOT template."""
hot_tpl = hot_tpl_complex_attrs
self.stack = parser.Stack(self.ctx,
'test_get_attr_invalid_none',
template.Template(hot_tpl))
self.stack.store()
self.stack.create()
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
self.stack.state)
snippet = {'Value': {'get_attr': ['resource2', 'who_cares']}}
self.assertRaises(exception.InvalidTemplateReference,
self.resolve, snippet)
def test_get_resource(self):
"""Test resolution of get_resource occurrences in HOT template."""
hot_tpl = hot_tpl_generic_resource
self.stack = parser.Stack(self.ctx, 'test_get_resource',
template.Template(hot_tpl))
self.stack.store()
self.stack.create()
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
self.stack.state)
snippet = {'value': {'get_resource': 'resource1'}}
self.assertEqual({'value': 'resource1'}, self.resolve(snippet))
def test_set_param_id(self):
tmpl = parser.Template(hot_tpl_empty)
self.stack = parser.Stack(self.ctx, 'param_id_test', tmpl)
self.assertEqual(self.stack.parameters['OS::stack_id'], 'None')
self.stack.store()
stack_identifier = self.stack.identifier()
self.assertEqual(self.stack.parameters['OS::stack_id'], self.stack.id)
self.assertEqual(self.stack.parameters['OS::stack_id'],
stack_identifier.stack_id)
self.m.VerifyAll()
def test_set_wrong_param(self):
tmpl = parser.Template(hot_tpl_empty)
stack_id = identifier.HeatIdentifier('', "stack_testit", None)
params = tmpl.parameters(None, {})
self.assertFalse(params.set_stack_id(None))
self.assertTrue(params.set_stack_id(stack_id))
def test_set_param_id_update(self):
tmpl = template.Template(
{'heat_template_version': '2013-05-23',
'resources': {'AResource': {'type': 'ResourceWithPropsType',
'metadata': {'Bar': {'get_param': 'OS::stack_id'}},
'properties': {'Foo': 'abc'}}}})
self.stack = parser.Stack(self.ctx, 'update_stack_id_test', tmpl)
self.stack.store()
self.stack.create()
self.assertEqual(self.stack.state,
(parser.Stack.CREATE, parser.Stack.COMPLETE))
stack_id = self.stack.parameters['OS::stack_id']
tmpl2 = template.Template(
{'heat_template_version': '2013-05-23',
'resources': {'AResource': {'type': 'ResourceWithPropsType',
'metadata': {'Bar': {'get_param': 'OS::stack_id'}},
'properties': {'Foo': 'xyz'}}}})
updated_stack = parser.Stack(self.ctx, 'updated_stack', tmpl2)
self.stack.update(updated_stack)
self.assertEqual(self.stack.state,
(parser.Stack.UPDATE, parser.Stack.COMPLETE))
self.assertEqual(self.stack['AResource'].properties['Foo'], 'xyz')
self.assertEqual(
self.stack['AResource'].metadata_get()['Bar'], stack_id)
def test_load_param_id(self):
tmpl = parser.Template(hot_tpl_empty)
self.stack = parser.Stack(self.ctx, 'param_load_id_test', tmpl)
self.stack.store()
stack_identifier = self.stack.identifier()
self.assertEqual(self.stack.parameters['OS::stack_id'],
stack_identifier.stack_id)
newstack = parser.Stack.load(self.ctx, stack_id=self.stack.id)
self.assertEqual(newstack.parameters['OS::stack_id'],
stack_identifier.stack_id)
def test_update_modify_param_ok_replace(self):
tmpl = {
'heat_template_version': '2013-05-23',
'parameters': {
'foo': {'type': 'string'}
},
'resources': {
'AResource': {
'type': 'ResourceWithPropsType',
'properties': {'Foo': {'get_param': 'foo'}}
}
}
}
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps,
'update_template_diff')
self.stack = parser.Stack(self.ctx, 'update_test_stack',
template.Template(tmpl),
environment.Environment({'foo': 'abc'}))
self.stack.store()
self.stack.create()
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
self.stack.state)
updated_stack = parser.Stack(self.ctx, 'updated_stack',
template.Template(tmpl),
environment.Environment({'foo': 'xyz'}))
def check_props(*args):
self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
generic_rsrc.ResourceWithProps.update_template_diff(
{'Type': 'ResourceWithPropsType',
'Properties': {'Foo': 'xyz'}},
{'Type': 'ResourceWithPropsType',
'Properties': {'Foo': 'abc'}}).WithSideEffects(check_props) \
.AndRaise(resource.UpdateReplace)
self.m.ReplayAll()
self.stack.update(updated_stack)
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
self.stack.state)
self.assertEqual('xyz', self.stack['AResource'].properties['Foo'])
self.m.VerifyAll()
class StackAttributesTest(HeatTestCase):
"""
Test stack get_attr function when stack was created from HOT template.
"""
def setUp(self):
super(StackAttributesTest, self).setUp()
self.ctx = utils.dummy_context()
resource._register_class('GenericResourceType',
generic_rsrc.GenericResource)
resource._register_class('ResourceWithComplexAttributesType',
generic_rsrc.ResourceWithComplexAttributes)
resource._register_class('ResWithComplexPropsAndAttrsType',
generic_rsrc.ResWithComplexPropsAndAttrs)
self.m.ReplayAll()
scenarios = [
('get_flat_attr',
dict(hot_tpl=hot_tpl_generic_resource,
snippet={'Value': {'get_attr': ['resource1', 'foo']}},
resource_name='resource1',
expected={'Value': 'resource1'})),
('get_list_attr',
dict(hot_tpl=hot_tpl_complex_attrs,
snippet={'Value': {'get_attr': ['resource1', 'list', 0]}},
resource_name='resource1',
expected={
'Value':
generic_rsrc.ResourceWithComplexAttributes.list[0]})),
('get_flat_dict_attr',
dict(hot_tpl=hot_tpl_complex_attrs,
snippet={'Value': {'get_attr': ['resource1',
'flat_dict',
'key2']}},
resource_name='resource1',
expected={
'Value':
generic_rsrc.ResourceWithComplexAttributes.
flat_dict['key2']})),
('get_nested_attr_list',
dict(hot_tpl=hot_tpl_complex_attrs,
snippet={'Value': {'get_attr': ['resource1',
'nested_dict',
'list',
0]}},
resource_name='resource1',
expected={
'Value':
generic_rsrc.ResourceWithComplexAttributes.
nested_dict['list'][0]})),
('get_nested_attr_dict',
dict(hot_tpl=hot_tpl_complex_attrs,
snippet={'Value': {'get_attr': ['resource1',
'nested_dict',
'dict',
'a']}},
resource_name='resource1',
expected={
'Value':
generic_rsrc.ResourceWithComplexAttributes.
nested_dict['dict']['a']})),
('get_attr_none',
dict(hot_tpl=hot_tpl_complex_attrs,
snippet={'Value': {'get_attr': ['resource1',
'none',
'who_cares']}},
resource_name='resource1',
expected={'Value': None}))
]
def test_get_attr(self):
"""Test resolution of get_attr occurrences in HOT template."""
self.stack = parser.Stack(self.ctx, 'test_get_attr',
template.Template(self.hot_tpl))
self.stack.store()
self.stack.create()
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
self.stack.state)
rsrc = self.stack[self.resource_name]
for action, status in (
(rsrc.CREATE, rsrc.IN_PROGRESS),
(rsrc.CREATE, rsrc.COMPLETE),
(rsrc.RESUME, rsrc.IN_PROGRESS),
(rsrc.RESUME, rsrc.COMPLETE),
(rsrc.UPDATE, rsrc.IN_PROGRESS),
(rsrc.UPDATE, rsrc.COMPLETE)):
rsrc.state_set(action, status)
resolved = function.resolve(self.stack.t.parse(self.stack,
self.snippet))
self.assertEqual(self.expected, resolved)
class StackGetAttrValidationTest(HeatTestCase):
def setUp(self):
super(StackGetAttrValidationTest, self).setUp()
self.ctx = utils.dummy_context()
resource._register_class('GenericResourceType',
generic_rsrc.GenericResource)
resource._register_class('ResWithComplexPropsAndAttrsType',
generic_rsrc.ResWithComplexPropsAndAttrs)
def test_validate_props_from_attrs(self):
stack = parser.Stack(self.ctx, 'test_props_from_attrs',
template.Template(hot_tpl_mapped_props))
stack.resources['resource1'].list = None
stack.resources['resource1'].map = None
stack.resources['resource1'].string = None
try:
stack.validate()
except exception.StackValidationFailed as exc:
self.fail("Validation should have passed: %s" % six.text_type(exc))
self.assertEqual([],
stack.resources['resource2'].properties['a_list'])
self.assertEqual({},
stack.resources['resource2'].properties['a_map'])
self.assertEqual('',
stack.resources['resource2'].properties['a_string'])
class StackParametersTest(HeatTestCase):
"""
Test stack get_param function when stack was created from HOT template.
"""
scenarios = [
('Ref_string',
dict(params={'foo': 'bar', 'blarg': 'wibble'},
snippet={'properties': {'prop1': {'Ref': 'foo'},
'prop2': {'Ref': 'blarg'}}},
expected={'properties': {'prop1': 'bar',
'prop2': 'wibble'}})),
('get_param_string',
dict(params={'foo': 'bar', 'blarg': 'wibble'},
snippet={'properties': {'prop1': {'get_param': 'foo'},
'prop2': {'get_param': 'blarg'}}},
expected={'properties': {'prop1': 'bar',
'prop2': 'wibble'}})),
('get_list_attr',
dict(params={'list': 'foo,bar'},
snippet={'properties': {'prop1': {'get_param': ['list', 1]}}},
expected={'properties': {'prop1': 'bar'}})),
('get_flat_dict_attr',
dict(params={'flat_dict':
{'key1': 'val1', 'key2': 'val2', 'key3': 'val3'}},
snippet={'properties': {'prop1': {'get_param':
['flat_dict', 'key2']}}},
expected={'properties': {'prop1': 'val2'}})),
('get_nested_attr_list',
dict(params={'nested_dict':
{'list': [1, 2, 3],
'string': 'abc',
'dict': {'a': 1, 'b': 2, 'c': 3}}},
snippet={'properties': {'prop1': {'get_param':
['nested_dict',
'list',
0]}}},
expected={'properties': {'prop1': 1}})),
('get_nested_attr_dict',
dict(params={'nested_dict':
{'list': [1, 2, 3],
'string': 'abc',
'dict': {'a': 1, 'b': 2, 'c': 3}}},
snippet={'properties': {'prop1': {'get_param':
['nested_dict',
'dict',
'a']}}},
expected={'properties': {'prop1': 1}})),
('get_attr_none',
dict(params={'none': None},
snippet={'properties': {'prop1': {'get_param':
['none',
'who_cares']}}},
expected={'properties': {'prop1': ''}})),
]
props_template = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
foo:
type: string
default: ''
blarg:
type: string
default: ''
list:
type: comma_delimited_list
default: ''
flat_dict:
type: json
default: {}
nested_dict:
type: json
default: {}
none:
type: string
default: 'default'
''')
def test_param_refs(self):
"""Test if parameter references work."""
tmpl = parser.Template(self.props_template)
env = environment.Environment(self.params)
stack = parser.Stack(utils.dummy_context(), 'test', tmpl, env)
self.assertEqual(self.expected,
function.resolve(tmpl.parse(stack, self.snippet)))
class HOTParamValidatorTest(HeatTestCase):
"""Test HOTParamValidator"""
def test_multiple_constraint_descriptions(self):
len_desc = 'string length should be between 8 and 16'
pattern_desc1 = 'Value must consist of characters only'
pattern_desc2 = 'Value must start with a lowercase character'
param = {
'db_name': {
'description': 'The WordPress database name',
'type': 'string',
'default': 'wordpress',
'constraints': [
{'length': {'min': 6, 'max': 16},
'description': len_desc},
{'allowed_pattern': '[a-zA-Z]+',
'description': pattern_desc1},
{'allowed_pattern': '[a-z]+[a-zA-Z]*',
'description': pattern_desc2}]}}
name = 'db_name'
schema = param['db_name']
def v(value):
param_schema = hot_param.HOTParamSchema.from_dict(name, schema)
param_schema.validate()
param_schema.validate_value(value)
return True
value = 'wp'
err = self.assertRaises(exception.StackValidationFailed, v, value)
self.assertIn(len_desc, six.text_type(err))
value = 'abcdefghijklmnopq'
err = self.assertRaises(exception.StackValidationFailed, v, value)
self.assertIn(len_desc, six.text_type(err))
value = 'abcdefgh1'
err = self.assertRaises(exception.StackValidationFailed, v, value)
self.assertIn(pattern_desc1, six.text_type(err))
value = 'Abcdefghi'
err = self.assertRaises(exception.StackValidationFailed, v, value)
self.assertIn(pattern_desc2, six.text_type(err))
value = 'abcdefghi'
self.assertTrue(v(value))
value = 'abcdefghI'
self.assertTrue(v(value))
def test_hot_template_validate_param(self):
len_desc = 'string length should be between 8 and 16'
pattern_desc1 = 'Value must consist of characters only'
pattern_desc2 = 'Value must start with a lowercase character'
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
db_name:
description: The WordPress database name
type: string
default: wordpress
constraints:
- length: { min: 8, max: 16 }
description: %s
- allowed_pattern: "[a-zA-Z]+"
description: %s
- allowed_pattern: "[a-z]+[a-zA-Z]*"
description: %s
''' % (len_desc, pattern_desc1, pattern_desc2))
tmpl = parser.Template(hot_tpl)
def run_parameters(value):
tmpl.parameters(
identifier.HeatIdentifier('', "stack_testit", None),
{'db_name': value}).validate(validate_value=True)
return True
value = 'wp'
err = self.assertRaises(exception.StackValidationFailed,
run_parameters, value)
self.assertIn(len_desc, six.text_type(err))
value = 'abcdefghijklmnopq'
err = self.assertRaises(exception.StackValidationFailed,
run_parameters, value)
self.assertIn(len_desc, six.text_type(err))
value = 'abcdefgh1'
err = self.assertRaises(exception.StackValidationFailed,
run_parameters, value)
self.assertIn(pattern_desc1, six.text_type(err))
value = 'Abcdefghi'
err = self.assertRaises(exception.StackValidationFailed,
run_parameters, value)
self.assertIn(pattern_desc2, six.text_type(err))
value = 'abcdefghi'
self.assertTrue(run_parameters(value))
value = 'abcdefghI'
self.assertTrue(run_parameters(value))
def test_range_constraint(self):
range_desc = 'Value must be between 30000 and 50000'
param = {
'db_port': {
'description': 'The database port',
'type': 'number',
'default': 31000,
'constraints': [
{'range': {'min': 30000, 'max': 50000},
'description': range_desc}]}}
name = 'db_port'
schema = param['db_port']
def v(value):
param_schema = hot_param.HOTParamSchema.from_dict(name, schema)
param_schema.validate()
param_schema.validate_value(value)
return True
value = 29999
err = self.assertRaises(exception.StackValidationFailed, v, value)
self.assertIn(range_desc, six.text_type(err))
value = 50001
err = self.assertRaises(exception.StackValidationFailed, v, value)
self.assertIn(range_desc, six.text_type(err))
value = 30000
self.assertTrue(v(value))
value = 40000
self.assertTrue(v(value))
value = 50000
self.assertTrue(v(value))
def test_custom_constraint(self):
class ZeroConstraint(object):
def validate(self, value, context):
return value == "0"
env = resources.global_env()
env.register_constraint("zero", ZeroConstraint)
self.addCleanup(env.constraints.pop, "zero")
desc = 'Value must be zero'
param = {
'param1': {
'type': 'string',
'constraints': [
{'custom_constraint': 'zero',
'description': desc}]}}
name = 'param1'
schema = param['param1']
def v(value):
param_schema = hot_param.HOTParamSchema.from_dict(name, schema)
param_schema.validate()
param_schema.validate_value(value)
return True
value = "1"
err = self.assertRaises(exception.StackValidationFailed, v, value)
self.assertEqual(desc, six.text_type(err))
value = "2"
err = self.assertRaises(exception.StackValidationFailed, v, value)
self.assertEqual(desc, six.text_type(err))
value = "0"
self.assertTrue(v(value))
def test_range_constraint_invalid_default(self):
range_desc = 'Value must be between 30000 and 50000'
param = {
'db_port': {
'description': 'The database port',
'type': 'number',
'default': 15,
'constraints': [
{'range': {'min': 30000, 'max': 50000},
'description': range_desc}]}}
schema = hot_param.HOTParamSchema.from_dict('db_port',
param['db_port'])
err = self.assertRaises(exception.InvalidSchemaError,
schema.validate)
self.assertIn(range_desc, six.text_type(err))
def test_validate_schema_wrong_key(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
foo: bar
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual("Invalid key 'foo' for parameter (param1)",
six.text_type(error))
def test_validate_schema_no_type(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
description: Hi!
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual("Missing parameter type for parameter: param1",
six.text_type(error))
def test_validate_schema_unknown_type(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
type: Unicode
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual(
"Invalid type (Unicode)", six.text_type(error))
def test_validate_schema_constraints(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
type: string
constraints:
- allowed_valus: [foo, bar]
default: foo
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual(
"Invalid key 'allowed_valus' for parameter constraints",
six.text_type(error))
def test_validate_schema_constraints_not_list(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
type: string
constraints: 1
default: foo
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual(
"Invalid parameter constraints for parameter param1, "
"expected a list", six.text_type(error))
def test_validate_schema_constraints_not_mapping(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
type: string
constraints: [foo]
default: foo
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual(
"Invalid parameter constraints, expected a mapping",
six.text_type(error))
def test_validate_schema_empty_constraints(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
type: string
constraints:
- description: a constraint
default: foo
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual("No constraint expressed", six.text_type(error))
def test_validate_schema_constraints_range_wrong_format(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
type: number
constraints:
- range: foo
default: foo
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual(
"Invalid range constraint, expected a mapping",
six.text_type(error))
def test_validate_schema_constraints_range_invalid_key(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
type: number
constraints:
- range: {min: 1, foo: bar}
default: 1
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual(
"Invalid key 'foo' for range constraint", six.text_type(error))
def test_validate_schema_constraints_length_wrong_format(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
type: string
constraints:
- length: foo
default: foo
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual(
"Invalid length constraint, expected a mapping",
six.text_type(error))
def test_validate_schema_constraints_length_invalid_key(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
type: string
constraints:
- length: {min: 1, foo: bar}
default: foo
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual(
"Invalid key 'foo' for length constraint", six.text_type(error))
def test_validate_schema_constraints_wrong_allowed_pattern(self):
hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
param1:
type: string
constraints:
- allowed_pattern: [foo, bar]
default: foo
''')
error = self.assertRaises(
exception.InvalidSchemaError, parameters.Parameters,
"stack_testit", parser.Template(hot_tpl))
self.assertEqual(
"AllowedPattern must be a string", six.text_type(error))