Merge "Add integration tests for preview update"
diff --git a/common/clients.py b/common/clients.py
index c7a7f60..daba708 100644
--- a/common/clients.py
+++ b/common/clients.py
@@ -28,7 +28,7 @@
calling various OpenStack APIs.
"""
- CINDERCLIENT_VERSION = '1'
+ CINDERCLIENT_VERSION = '2'
HEATCLIENT_VERSION = '1'
NOVACLIENT_VERSION = '2'
CEILOMETER_VERSION = '2'
diff --git a/common/test.py b/common/test.py
index 5e37a39..4f3d923 100644
--- a/common/test.py
+++ b/common/test.py
@@ -250,9 +250,9 @@
def _verify_status(self, stack, stack_identifier, status, fail_regexp):
if stack.stack_status == status:
- # Handle UPDATE_COMPLETE case: Make sure we don't
- # wait for a stale UPDATE_COMPLETE status.
- if status == 'UPDATE_COMPLETE':
+ # Handle UPDATE_COMPLETE/FAILED case: Make sure we don't
+ # wait for a stale UPDATE_COMPLETE/FAILED status.
+ if status in ('UPDATE_FAILED', 'UPDATE_COMPLETE'):
if self.updated_time.get(
stack_identifier) != stack.updated_time:
self.updated_time[stack_identifier] = stack.updated_time
@@ -263,8 +263,8 @@
wait_for_action = status.split('_')[0]
if (stack.action == wait_for_action and
fail_regexp.search(stack.stack_status)):
- # Handle UPDATE_FAILED case.
- if status == 'UPDATE_FAILED':
+ # Handle UPDATE_COMPLETE/UPDATE_FAILED case.
+ if status in ('UPDATE_FAILED', 'UPDATE_COMPLETE'):
if self.updated_time.get(
stack_identifier) != stack.updated_time:
self.updated_time[stack_identifier] = stack.updated_time
@@ -279,7 +279,7 @@
stack_status_reason=stack.stack_status_reason)
def _wait_for_stack_status(self, stack_identifier, status,
- failure_pattern='^.*_FAILED$',
+ failure_pattern=None,
success_on_not_found=False):
"""
Waits for a Stack to reach a given status.
@@ -288,7 +288,13 @@
CREATE_COMPLETE, not just COMPLETE which is exposed
via the status property of Stack in heatclient
"""
- fail_regexp = re.compile(failure_pattern)
+ if failure_pattern:
+ fail_regexp = re.compile(failure_pattern)
+ elif 'FAILED' in status:
+ # If we're looking for e.g CREATE_FAILED, COMPLETE is unexpected.
+ fail_regexp = re.compile('^.*_COMPLETE$')
+ else:
+ fail_regexp = re.compile('^.*_FAILED$')
build_timeout = self.conf.build_timeout
build_interval = self.conf.build_interval
@@ -323,10 +329,11 @@
stack_identifier, 'DELETE_COMPLETE',
success_on_not_found=True)
- def update_stack(self, stack_identifier, template, environment=None,
+ def update_stack(self, stack_identifier, template=None, environment=None,
files=None, parameters=None, tags=None,
expected_status='UPDATE_COMPLETE',
- disable_rollback=True):
+ disable_rollback=True,
+ existing=False):
env = environment or {}
env_files = files or {}
parameters = parameters or {}
@@ -335,6 +342,8 @@
build_timeout = self.conf.build_timeout
build_interval = self.conf.build_interval
start = timeutils.utcnow()
+ self.updated_time[stack_identifier] = self.client.stacks.get(
+ stack_identifier).updated_time
while timeutils.delta_seconds(start,
timeutils.utcnow()) < build_timeout:
try:
@@ -346,7 +355,8 @@
disable_rollback=disable_rollback,
parameters=parameters,
environment=env,
- tags=tags
+ tags=tags,
+ existing=existing
)
except heat_exceptions.HTTPConflict as ex:
# FIXME(sirushtim): Wait a little for the stack lock to be
diff --git a/functional/test_autoscaling.py b/functional/test_autoscaling.py
index 1b9fe99..9041405 100644
--- a/functional/test_autoscaling.py
+++ b/functional/test_autoscaling.py
@@ -13,7 +13,9 @@
import copy
import json
+from heatclient import exc
from oslo_log import log as logging
+import six
from testtools import matchers
from heat_integrationtests.common import test
@@ -728,8 +730,13 @@
self._wait_for_resource_status(
stack_identifier, 'JobServerGroup', 'SUSPEND_COMPLETE')
- # Send a signal and confirm nothing happened.
- self.client.resources.signal(stack_identifier, 'ScaleUpPolicy')
+ # Send a signal and a exception will raise
+ ex = self.assertRaises(exc.BadRequest,
+ self.client.resources.signal,
+ stack_identifier, 'ScaleUpPolicy')
+
+ error_msg = 'Signal resource during SUSPEND is not supported'
+ self.assertIn(error_msg, six.text_type(ex))
ev = self.wait_for_event_with_reason(
stack_identifier,
reason='Cannot signal resource during SUSPEND',
diff --git a/functional/test_create_update.py b/functional/test_create_update.py
index b2aad34..4597d4c 100644
--- a/functional/test_create_update.py
+++ b/functional/test_create_update.py
@@ -102,11 +102,15 @@
provider_group_template = '''
heat_template_version: 2013-05-23
+parameters:
+ count:
+ type: number
+ default: 2
resources:
test_group:
type: OS::Heat::ResourceGroup
properties:
- count: 2
+ count: {get_param: count}
resource_def:
type: My::TestResource
'''
@@ -134,6 +138,20 @@
user_data: {get_param: user_data}
'''
+ fail_param_template = '''
+heat_template_version: 2014-10-16
+parameters:
+ do_fail:
+ type: boolean
+ default: False
+resources:
+ aresource:
+ type: OS::Heat::TestResource
+ properties:
+ value: Test
+ fail: {get_param: do_fail}
+'''
+
def setUp(self):
super(UpdateStackTest, self).setUp()
@@ -243,6 +261,29 @@
self.assertEqual(updated_resources,
self.list_resources(stack_identifier))
+ def test_stack_update_from_failed(self):
+ # Prove it's possible to update from an UPDATE_FAILED state
+ template = _change_rsrc_properties(test_template_one_resource,
+ ['test1'],
+ {'value': 'test_update_failed'})
+ stack_identifier = self.stack_create(
+ template=template)
+ initial_resources = {'test1': 'OS::Heat::TestResource'}
+ self.assertEqual(initial_resources,
+ self.list_resources(stack_identifier))
+
+ tmpl_update = _change_rsrc_properties(
+ test_template_one_resource, ['test1'], {'fail': True})
+ # Update with bad template, we should fail
+ self.update_stack(stack_identifier, tmpl_update,
+ expected_status='UPDATE_FAILED')
+ # but then passing a good template should succeed
+ self.update_stack(stack_identifier, test_template_two_resource)
+ updated_resources = {'test1': 'OS::Heat::TestResource',
+ 'test2': 'OS::Heat::TestResource'}
+ self.assertEqual(updated_resources,
+ self.list_resources(stack_identifier))
+
def test_stack_update_provider(self):
template = _change_rsrc_properties(
test_template_one_resource, ['test1'],
@@ -294,7 +335,7 @@
'''Test two-level nested update.'''
# Create a ResourceGroup (which creates a nested stack),
# containing provider resources (which create a nested
- # stack), thus excercising an update which traverses
+ # stack), thus exercising an update which traverses
# two levels of nesting.
template = _change_rsrc_properties(
test_template_one_resource, ['test1'],
@@ -391,3 +432,69 @@
stack_identifier,
template=self.update_userdata_template,
parameters=parms_updated)
+
+ def test_stack_update_provider_group_patch(self):
+ '''Test two-level nested update with PATCH'''
+ template = _change_rsrc_properties(
+ test_template_one_resource, ['test1'],
+ {'value': 'test_provider_group_template'})
+ files = {'provider.template': json.dumps(template)}
+ env = {'resource_registry':
+ {'My::TestResource': 'provider.template'}}
+
+ stack_identifier = self.stack_create(
+ template=self.provider_group_template,
+ files=files,
+ environment=env
+ )
+
+ initial_resources = {'test_group': 'OS::Heat::ResourceGroup'}
+ self.assertEqual(initial_resources,
+ self.list_resources(stack_identifier))
+
+ # Prove the resource is backed by a nested stack, save the ID
+ nested_identifier = self.assert_resource_is_a_stack(stack_identifier,
+ 'test_group')
+
+ # Then check the expected resources are in the nested stack
+ nested_resources = {'0': 'My::TestResource',
+ '1': 'My::TestResource'}
+ self.assertEqual(nested_resources,
+ self.list_resources(nested_identifier))
+
+ # increase the count, pass only the paramter, no env or template
+ params = {'count': 3}
+ self.update_stack(stack_identifier, parameters=params, existing=True)
+
+ # Parent resources should be unchanged and the nested stack
+ # should have been updated in-place without replacement
+ self.assertEqual(initial_resources,
+ self.list_resources(stack_identifier))
+
+ # Resource group stack should also be unchanged (but updated)
+ nested_stack = self.client.stacks.get(nested_identifier)
+ self.assertEqual('UPDATE_COMPLETE', nested_stack.stack_status)
+ # Add a resource, as we should have added one
+ nested_resources['2'] = 'My::TestResource'
+ self.assertEqual(nested_resources,
+ self.list_resources(nested_identifier))
+
+ def test_stack_update_from_failed_patch(self):
+ '''Test PATCH update from a failed state.'''
+
+ # Start with empty template
+ stack_identifier = self.stack_create(
+ template='heat_template_version: 2014-10-16')
+
+ # Update with a good template, but bad parameter
+ self.update_stack(stack_identifier,
+ template=self.fail_param_template,
+ parameters={'do_fail': True},
+ expected_status='UPDATE_FAILED')
+
+ # PATCH update, only providing the parameter
+ self.update_stack(stack_identifier,
+ parameters={'do_fail': False},
+ existing=True)
+ self.assertEqual({u'aresource': u'OS::Heat::TestResource'},
+ self.list_resources(stack_identifier))
diff --git a/functional/test_create_update_neutron_port.py b/functional/test_create_update_neutron_port.py
index 4b2df59..575d21c 100644
--- a/functional/test_create_update_neutron_port.py
+++ b/functional/test_create_update_neutron_port.py
@@ -10,8 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from testtools import testcase
-
from heat_integrationtests.functional import functional_base
@@ -38,6 +36,12 @@
fixed_ips:
- subnet: {get_resource: subnet}
ip_address: 11.11.11.11
+ test:
+ depends_on: port
+ type: OS::Heat::TestResource
+ properties:
+ value: Test1
+ fail: False
outputs:
port_ip:
value: {get_attr: [port, fixed_ips, 0, ip_address]}
@@ -73,7 +77,6 @@
self.assertNotEqual(_ip, new_ip)
self.assertNotEqual(_id, new_id)
- @testcase.skip('Skipped until bug #1455100 is fixed.')
def test_stack_update_replace_with_ip(self):
# create with default 'mac' parameter
stack_identifier = self.stack_create(template=test_template)
@@ -92,6 +95,62 @@
self.assertEqual(_ip, new_ip)
self.assertNotEqual(_id, new_id)
+ def test_stack_update_replace_with_ip_rollback(self):
+ # create with default 'mac' parameter
+ stack_identifier = self.stack_create(template=test_template)
+
+ _id, _ip = self.get_port_id_and_ip(stack_identifier)
+
+ # Update with another 'mac' parameter
+ parameters = {'mac': '00-00-00-00-AA-AA'}
+
+ # make test resource failing during update
+ fail_template = test_template.replace('fail: False',
+ 'fail: True')
+ fail_template = fail_template.replace('value: Test1',
+ 'value: Rollback')
+
+ # port should be replaced with same ip
+ self.update_stack(stack_identifier, fail_template,
+ parameters=parameters,
+ expected_status='ROLLBACK_COMPLETE',
+ disable_rollback=False)
+
+ new_id, new_ip = self.get_port_id_and_ip(stack_identifier)
+ # port id and ip should be the same after rollback
+ self.assertEqual(_ip, new_ip)
+ self.assertEqual(_id, new_id)
+
+ def test_stack_update_replace_with_ip_after_failed_update(self):
+ # create with default 'mac' parameter
+ stack_identifier = self.stack_create(template=test_template)
+
+ _id, _ip = self.get_port_id_and_ip(stack_identifier)
+
+ # Update with another 'mac' parameter
+ parameters = {'mac': '00-00-00-00-AA-AA'}
+
+ # make test resource failing during update
+ fail_template = test_template.replace('fail: False',
+ 'fail: True')
+ fail_template = fail_template.replace('value: Test1',
+ 'value: Rollback')
+
+ # port should be replaced with same ip
+ self.update_stack(stack_identifier, fail_template,
+ parameters=parameters,
+ expected_status='UPDATE_FAILED')
+
+ # port should be replaced with same ip
+ self.update_stack(stack_identifier, test_template,
+ parameters=parameters)
+
+ new_id, new_ip = self.get_port_id_and_ip(stack_identifier)
+ # ip should be the same, but port id should be different, because it's
+ # restore replace
+ self.assertEqual(_ip, new_ip)
+ self.assertNotEqual(_id, new_id)
+
def test_stack_update_in_place_remove_ip(self):
# create with default 'mac' parameter and defined ip_address
stack_identifier = self.stack_create(template=test_template)
diff --git a/functional/test_stack_events.py b/functional/test_stack_events.py
new file mode 100644
index 0000000..b1b2339
--- /dev/null
+++ b/functional/test_stack_events.py
@@ -0,0 +1,112 @@
+# 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_integrationtests.functional import functional_base
+
+
+class StackEventsTest(functional_base.FunctionalTestsBase):
+
+ template = '''
+heat_template_version: 2014-10-16
+parameters:
+resources:
+ test_resource:
+ type: OS::Heat::TestResource
+ properties:
+ value: 'test1'
+ fail: False
+ update_replace: False
+ wait_secs: 0
+outputs:
+ resource_id:
+ description: 'ID of resource'
+ value: { get_resource: test_resource }
+'''
+
+ def setUp(self):
+ super(StackEventsTest, self).setUp()
+
+ def _verify_event_fields(self, event, event_characteristics):
+ self.assertIsNotNone(event_characteristics)
+ self.assertIsNotNone(event.event_time)
+ self.assertIsNotNone(event.links)
+ self.assertIsNotNone(event.logical_resource_id)
+ self.assertIsNotNone(event.resource_status)
+ self.assertIn(event.resource_status, event_characteristics[1])
+ self.assertIsNotNone(event.resource_status_reason)
+ self.assertIsNotNone(event.id)
+
+ def test_event(self):
+ parameters = {}
+
+ test_stack_name = self._stack_rand_name()
+ stack_identifier = self.stack_create(
+ stack_name=test_stack_name,
+ template=self.template,
+ parameters=parameters
+ )
+
+ expected_status = ['CREATE_IN_PROGRESS', 'CREATE_COMPLETE']
+ event_characteristics = {
+ test_stack_name: ('OS::Heat::Stack', expected_status),
+ 'test_resource': ('OS::Heat::TestResource', expected_status)}
+
+ # List stack events
+ # API: GET /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/events
+ stack_events = self.client.events.list(stack_identifier)
+
+ for stack_event in stack_events:
+ # Key on an expected/valid resource name
+ self._verify_event_fields(
+ stack_event,
+ event_characteristics[stack_event.resource_name])
+
+ # Test the event filtering API based on this resource_name
+ # /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/resources/{resource_name}/events
+ resource_events = self.client.events.list(
+ stack_identifier,
+ stack_event.resource_name)
+
+ # Resource events are a subset of the original stack event list
+ self.assertTrue(len(resource_events) < len(stack_events))
+
+ # Get the event details for each resource event
+ for resource_event in resource_events:
+ # A resource_event should be in the original stack event list
+ self.assertIn(resource_event, stack_events)
+ # Given a filtered list, the resource names should be identical
+ self.assertEqual(
+ resource_event.resource_name,
+ stack_event.resource_name)
+ # Verify all fields, keying off the resource_name
+ self._verify_event_fields(
+ resource_event,
+ event_characteristics[resource_event.resource_name])
+
+ # Exercise the event details API
+ # /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/resources/{resource_name}/events/{event_id}
+ event_details = self.client.events.get(
+ stack_identifier,
+ resource_event.resource_name,
+ resource_event.id)
+ self._verify_event_fields(
+ event_details,
+ event_characteristics[event_details.resource_name])
+ # The names should be identical to the non-detailed event
+ self.assertEqual(
+ resource_event.resource_name,
+ event_details.resource_name)
+ # Verify the extra field in the detail results
+ self.assertIsNotNone(event_details.resource_type)
+ self.assertEqual(
+ event_characteristics[event_details.resource_name][0],
+ event_details.resource_type)
diff --git a/functional/test_stack_tags.py b/functional/test_stack_tags.py
index 05600f5..4a97798 100644
--- a/functional/test_stack_tags.py
+++ b/functional/test_stack_tags.py
@@ -19,11 +19,18 @@
heat_template_version: 2014-10-16
description:
foo
+parameters:
+ input:
+ type: string
+ default: test
+resources:
+ not-used:
+ type: OS::Heat::TestResource
+ properties:
+ wait_secs: 1
+ value: {get_param: input}
'''
- def setUp(self):
- super(StackTagTest, self).setUp()
-
def test_stack_tag(self):
# Stack create with stack tags
tags = 'foo,bar'
@@ -41,7 +48,8 @@
self.update_stack(
stack_identifier,
template=self.template,
- tags=updated_tags)
+ tags=updated_tags,
+ parameters={'input': 'next'})
# Ensure property tag is populated and matches updated tags
updated_stack = self.client.stacks.get(stack_identifier)
@@ -50,7 +58,8 @@
# Delete tags
self.update_stack(
stack_identifier,
- template=self.template
+ template=self.template,
+ parameters={'input': 'none'}
)
# Ensure property tag is not populated
diff --git a/functional/test_template_resource.py b/functional/test_template_resource.py
index a967888..9609664 100644
--- a/functional/test_template_resource.py
+++ b/functional/test_template_resource.py
@@ -12,6 +12,8 @@
import json
+from heatclient import exc as heat_exceptions
+import six
import yaml
from heat_integrationtests.common import test
@@ -243,6 +245,42 @@
self.assertEqual(old_way, test_attr2)
+class TemplateResourceFacadeTest(functional_base.FunctionalTestsBase):
+ """Prove that we can use ResourceFacade in a HOT template."""
+
+ main_template = '''
+heat_template_version: 2013-05-23
+resources:
+ the_nested:
+ type: the.yaml
+ metadata:
+ foo: bar
+outputs:
+ value:
+ value: {get_attr: [the_nested, output]}
+'''
+
+ nested_templ = '''
+heat_template_version: 2013-05-23
+resources:
+ test:
+ type: OS::Heat::TestResource
+ properties:
+ value: {"Fn::Select": [foo, {resource_facade: metadata}]}
+outputs:
+ output:
+ value: {get_attr: [test, output]}
+ '''
+
+ def test_metadata(self):
+ stack_identifier = self.stack_create(
+ template=self.main_template,
+ files={'the.yaml': self.nested_templ})
+ stack = self.client.stacks.get(stack_identifier)
+ value = self._stack_output(stack, 'value')
+ self.assertEqual('bar', value)
+
+
class TemplateResourceUpdateTest(functional_base.FunctionalTestsBase):
"""Prove that we can do template resource updates."""
@@ -713,3 +751,91 @@
self.stack_suspend(stack_identifier=stack_identifier)
self.stack_resume(stack_identifier=stack_identifier)
+
+
+class ValidateFacadeTest(test.HeatIntegrationTest):
+ """Prove that nested stack errors don't suck."""
+ template = '''
+heat_template_version: 2015-10-15
+resources:
+ thisone:
+ type: OS::Thingy
+ properties:
+ one: pre
+ two: post
+outputs:
+ one:
+ value: {get_attr: [thisone, here-it-is]}
+'''
+ templ_facade = '''
+heat_template_version: 2015-04-30
+parameters:
+ one:
+ type: string
+ two:
+ type: string
+outputs:
+ here-it-is:
+ value: noop
+'''
+ env = '''
+resource_registry:
+ OS::Thingy: facade.yaml
+ resources:
+ thisone:
+ OS::Thingy: concrete.yaml
+'''
+
+ def setUp(self):
+ super(ValidateFacadeTest, self).setUp()
+ self.client = self.orchestration_client
+
+ def test_missing_param(self):
+ templ_missing_parameter = '''
+heat_template_version: 2015-04-30
+parameters:
+ one:
+ type: string
+resources:
+ str:
+ type: OS::Heat::RandomString
+outputs:
+ here-it-is:
+ value:
+ not-important
+'''
+ try:
+ self.stack_create(
+ template=self.template,
+ environment=self.env,
+ files={'facade.yaml': self.templ_facade,
+ 'concrete.yaml': templ_missing_parameter},
+ expected_status='CREATE_FAILED')
+ except heat_exceptions.HTTPBadRequest as exc:
+ exp = ('ERROR: Required property two for facade '
+ 'OS::Thingy missing in provider')
+ self.assertEqual(exp, six.text_type(exc))
+
+ def test_missing_output(self):
+ templ_missing_output = '''
+heat_template_version: 2015-04-30
+parameters:
+ one:
+ type: string
+ two:
+ type: string
+resources:
+ str:
+ type: OS::Heat::RandomString
+'''
+ try:
+ self.stack_create(
+ template=self.template,
+ environment=self.env,
+ files={'facade.yaml': self.templ_facade,
+ 'concrete.yaml': templ_missing_output},
+ expected_status='CREATE_FAILED')
+ except heat_exceptions.HTTPBadRequest as exc:
+ exp = ('ERROR: Attribute here-it-is for facade '
+ 'OS::Thingy missing in provider')
+ self.assertEqual(exp, six.text_type(exc))
diff --git a/functional/test_template_validate.py b/functional/test_template_validate.py
new file mode 100644
index 0000000..e62c31b
--- /dev/null
+++ b/functional/test_template_validate.py
@@ -0,0 +1,244 @@
+# 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 heatclient import exc
+
+from heat_integrationtests.functional import functional_base
+
+
+class StackTemplateValidateTest(functional_base.FunctionalTestsBase):
+
+ random_template = '''
+heat_template_version: 2014-10-16
+description: the stack description
+parameters:
+ aparam:
+ type: number
+ default: 10
+ description: the param description
+resources:
+ myres:
+ type: OS::Heat::RandomString
+ properties:
+ length: {get_param: aparam}
+'''
+
+ parent_template = '''
+heat_template_version: 2014-10-16
+description: the parent template
+parameters:
+ pparam:
+ type: number
+ default: 5
+ description: the param description
+resources:
+ nres:
+ type: mynested.yaml
+ properties:
+ aparam: {get_param: pparam}
+'''
+
+ parent_template_noprop = '''
+heat_template_version: 2014-10-16
+description: the parent template
+resources:
+ nres:
+ type: mynested.yaml
+'''
+
+ random_template_groups = '''
+heat_template_version: 2014-10-16
+description: the stack description
+parameters:
+ aparam:
+ type: number
+ default: 10
+ description: the param description
+ bparam:
+ type: string
+ default: foo
+ cparam:
+ type: string
+ default: secret
+ hidden: true
+parameter_groups:
+- label: str_params
+ description: The string params
+ parameters:
+ - bparam
+ - cparam
+resources:
+ myres:
+ type: OS::Heat::RandomString
+ properties:
+ length: {get_param: aparam}
+'''
+
+ def test_template_validate_basic(self):
+ ret = self.client.stacks.validate(template=self.random_template)
+ expected = {'Description': 'the stack description',
+ 'Parameters': {
+ 'aparam': {'Default': 10,
+ 'Description': 'the param description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_override_default(self):
+ env = {'parameters': {'aparam': 5}}
+ ret = self.client.stacks.validate(template=self.random_template,
+ environment=env)
+ expected = {'Description': 'the stack description',
+ 'Parameters': {
+ 'aparam': {'Default': 10,
+ 'Value': 5,
+ 'Description': 'the param description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_override_none(self):
+ env = {'resource_registry': {
+ 'OS::Heat::RandomString': 'OS::Heat::None'}}
+ ret = self.client.stacks.validate(template=self.random_template,
+ environment=env)
+ expected = {'Description': 'the stack description',
+ 'Parameters': {
+ 'aparam': {'Default': 10,
+ 'Description': 'the param description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_basic_required_param(self):
+ tmpl = self.random_template.replace('default: 10', '')
+ ret = self.client.stacks.validate(template=tmpl)
+ expected = {'Description': 'the stack description',
+ 'Parameters': {
+ 'aparam': {'Description': 'the param description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_fail_version(self):
+ fail_template = self.random_template.replace('2014-10-16', 'invalid')
+ ex = self.assertRaises(exc.HTTPBadRequest,
+ self.client.stacks.validate,
+ template=fail_template)
+ self.assertIn('The template version is invalid', six.text_type(ex))
+
+ def test_template_validate_parameter_groups(self):
+ ret = self.client.stacks.validate(template=self.random_template_groups)
+ expected = {'Description': 'the stack description',
+ 'ParameterGroups':
+ [{'description': 'The string params',
+ 'label': 'str_params',
+ 'parameters': ['bparam', 'cparam']}],
+ 'Parameters':
+ {'aparam':
+ {'Default': 10,
+ 'Description': 'the param description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'},
+ 'bparam':
+ {'Default': 'foo',
+ 'Description': '',
+ 'Label': 'bparam',
+ 'NoEcho': 'false',
+ 'Type': 'String'},
+ 'cparam':
+ {'Default': 'secret',
+ 'Description': '',
+ 'Label': 'cparam',
+ 'NoEcho': 'true',
+ 'Type': 'String'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_nested_off(self):
+ files = {'mynested.yaml': self.random_template}
+ ret = self.client.stacks.validate(template=self.parent_template,
+ files=files)
+ expected = {'Description': 'the parent template',
+ 'Parameters': {
+ 'pparam': {'Default': 5,
+ 'Description': 'the param description',
+ 'Label': 'pparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_nested_on(self):
+ files = {'mynested.yaml': self.random_template}
+ ret = self.client.stacks.validate(template=self.parent_template_noprop,
+ files=files,
+ show_nested=True)
+ expected = {'Description': 'the parent template',
+ 'Parameters': {},
+ 'NestedParameters': {
+ 'nres': {'Description': 'the stack description',
+ 'Parameters': {'aparam': {'Default': 10,
+ 'Description':
+ 'the param '
+ 'description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}},
+ 'Type': 'mynested.yaml'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_nested_on_multiple(self):
+ # parent_template -> nested_template -> random_template
+ nested_template = self.random_template.replace(
+ 'OS::Heat::RandomString', 'mynested2.yaml')
+ files = {'mynested.yaml': nested_template,
+ 'mynested2.yaml': self.random_template}
+ ret = self.client.stacks.validate(template=self.parent_template,
+ files=files,
+ show_nested=True)
+
+ n_param2 = {'myres': {'Description': 'the stack description',
+ 'Parameters': {'aparam': {'Default': 10,
+ 'Description':
+ 'the param '
+ 'description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}},
+ 'Type': 'mynested2.yaml'}}
+ expected = {'Description': 'the parent template',
+ 'Parameters': {
+ 'pparam': {'Default': 5,
+ 'Description': 'the param description',
+ 'Label': 'pparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}},
+ 'NestedParameters': {
+ 'nres': {'Description': 'the stack description',
+ 'Parameters': {'aparam': {'Default': 10,
+ 'Description':
+ 'the param '
+ 'description',
+ 'Label': 'aparam',
+ 'Value': 5,
+ 'NoEcho': 'false',
+ 'Type': 'Number'}},
+ 'NestedParameters': n_param2,
+ 'Type': 'mynested.yaml'}}}
+ self.assertEqual(expected, ret)