Merge "Fix test_encryption_vol_type to take admin creds from conf"
diff --git a/common/test.py b/common/test.py
index 8fc5c60..4eaa2b7 100644
--- a/common/test.py
+++ b/common/test.py
@@ -257,6 +257,14 @@
                    (resource_name, status, build_timeout))
         raise exceptions.TimeoutException(message)
 
+    def verify_resource_status(self, stack_identifier, resource_name,
+                               status='CREATE_COMPLETE'):
+        try:
+            res = self.client.resources.get(stack_identifier, resource_name)
+        except heat_exceptions.HTTPNotFound:
+            return False
+        return res.resource_status == status
+
     def _verify_status(self, stack, stack_identifier, status, fail_regexp):
         if stack.stack_status == status:
             # Handle UPDATE_COMPLETE/FAILED case: Make sure we don't
@@ -310,7 +318,8 @@
         while timeutils.delta_seconds(start,
                                       timeutils.utcnow()) < build_timeout:
             try:
-                stack = self.client.stacks.get(stack_identifier)
+                stack = self.client.stacks.get(stack_identifier,
+                                               resolve_outputs=False)
             except heat_exceptions.HTTPNotFound:
                 if success_on_not_found:
                     return
@@ -367,7 +376,7 @@
         stack_name = stack_identifier.split('/')[0]
 
         self.updated_time[stack_identifier] = self.client.stacks.get(
-            stack_identifier).updated_time
+            stack_identifier, resolve_outputs=False).updated_time
 
         self._handle_in_progress(
             self.client.stacks.update,
@@ -439,7 +448,7 @@
         nested_identifier = '/'.join(nested_href.split('/')[-2:])
         self.assertEqual(rsrc.physical_resource_id, nested_id)
 
-        nested_stack = self.client.stacks.get(nested_id)
+        nested_stack = self.client.stacks.get(nested_id, resolve_outputs=False)
         nested_identifier2 = '%s/%s' % (nested_stack.stack_name,
                                         nested_stack.id)
         self.assertEqual(nested_identifier, nested_identifier2)
@@ -453,7 +462,8 @@
         rsrc = self.client.resources.get(stack_identifier, group_name)
         physical_resource_id = rsrc.physical_resource_id
 
-        nested_stack = self.client.stacks.get(physical_resource_id)
+        nested_stack = self.client.stacks.get(physical_resource_id,
+                                              resolve_outputs=False)
         nested_identifier = '%s/%s' % (nested_stack.stack_name,
                                        nested_stack.id)
         parent_id = stack_identifier.split("/")[-1]
@@ -475,7 +485,8 @@
     def stack_create(self, stack_name=None, template=None, files=None,
                      parameters=None, environment=None, tags=None,
                      expected_status='CREATE_COMPLETE',
-                     disable_rollback=True, enable_cleanup=True):
+                     disable_rollback=True, enable_cleanup=True,
+                     environment_files=None):
         name = stack_name or self._stack_rand_name()
         templ = template or self.template
         templ_files = files or {}
@@ -488,12 +499,13 @@
             disable_rollback=disable_rollback,
             parameters=params,
             environment=env,
-            tags=tags
+            tags=tags,
+            environment_files=environment_files
         )
         if expected_status not in ['ROLLBACK_COMPLETE'] and enable_cleanup:
             self.addCleanup(self._stack_delete, name)
 
-        stack = self.client.stacks.get(name)
+        stack = self.client.stacks.get(name, resolve_outputs=False)
         stack_identifier = '%s/%s' % (name, stack.id)
         kwargs = {'stack_identifier': stack_identifier,
                   'status': expected_status}
@@ -524,8 +536,7 @@
             adopt_stack_data=adopt_data,
         )
         self.addCleanup(self._stack_delete, name)
-
-        stack = self.client.stacks.get(name)
+        stack = self.client.stacks.get(name, resolve_outputs=False)
         stack_identifier = '%s/%s' % (name, stack.id)
         self._wait_for_stack_status(stack_identifier, wait_for_status)
         return stack_identifier
diff --git a/functional/test_default_parameters.py b/functional/test_default_parameters.py
index a33823f..7201969 100644
--- a/functional/test_default_parameters.py
+++ b/functional/test_default_parameters.py
@@ -77,7 +77,7 @@
             # remove the default from the parameter in the nested template.
             ntempl = yaml.safe_load(self.nested_template)
             del ntempl['parameters']['length']['default']
-            nested_template = yaml.dump(ntempl)
+            nested_template = yaml.safe_dump(ntempl)
         else:
             nested_template = self.nested_template
 
diff --git a/functional/test_env_merge.py b/functional/test_env_merge.py
new file mode 100644
index 0000000..5e222b8
--- /dev/null
+++ b/functional/test_env_merge.py
@@ -0,0 +1,95 @@
+#
+#    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
+
+
+TEMPLATE = '''
+    heat_template_version: 2015-04-30
+    parameters:
+      p0:
+        type: string
+        default: CORRECT
+      p1:
+        type: string
+        default: INCORRECT
+      p2:
+        type: string
+        default: INCORRECT
+    resources:
+      r1:
+        type: test::R1
+      r2:
+        type: test::R2
+      r3a:
+        type: test::R3
+      r3b:
+        type: test::R3
+'''
+
+ENV_1 = '''
+    parameters:
+      p1: CORRECT
+      p2: INCORRECT-E1
+    resource_registry:
+      test::R1: OS::Heat::RandomString
+      test::R2: BROKEN
+      test::R3: OS::Heat::None
+'''
+
+ENV_2 = '''
+    parameters:
+      p2: CORRECT
+    resource_registry:
+      test::R2: OS::Heat::RandomString
+      resources:
+        r3b:
+          test::R3: OS::Heat::RandomString
+'''
+
+
+class EnvironmentMergingTests(functional_base.FunctionalTestsBase):
+
+    def test_server_environment_merging(self):
+
+        # Setup
+        files = {'env1.yaml': ENV_1, 'env2.yaml': ENV_2}
+        environment_files = ['env1.yaml', 'env2.yaml']
+
+        # Test
+        stack_id = self.stack_create(stack_name='env_merge',
+                                     template=TEMPLATE,
+                                     files=files,
+                                     environment_files=environment_files)
+
+        # Verify
+
+        # Since there is no environment show, the registry overriding
+        # is partially verified by there being no error. If it wasn't
+        # working, test::R2 would remain mapped to BROKEN in env1.
+
+        # Sanity check
+        resources = self.list_resources(stack_id)
+        self.assertEqual(4, len(resources))
+
+        # Verify the parameters are correctly set
+        stack = self.client.stacks.get(stack_id)
+        self.assertEqual('CORRECT', stack.parameters['p0'])
+        self.assertEqual('CORRECT', stack.parameters['p1'])
+        self.assertEqual('CORRECT', stack.parameters['p2'])
+
+        # Verify that r3b has been overridden into a RandomString
+        # by checking to see that it has a value
+        r3b = self.client.resources.get(stack_id, 'r3b')
+        r3b_attrs = r3b.attributes
+        self.assertTrue('value' in r3b_attrs)
diff --git a/functional/test_event_sinks.py b/functional/test_event_sinks.py
new file mode 100644
index 0000000..e4a23ff
--- /dev/null
+++ b/functional/test_event_sinks.py
@@ -0,0 +1,66 @@
+#    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 uuid
+
+from zaqarclient.queues.v1 import client as zaqarclient
+
+from heat_integrationtests.functional import functional_base
+
+
+class ZaqarEventSinkTest(functional_base.FunctionalTestsBase):
+    template = '''
+heat_template_version: "2013-05-23"
+resources:
+  test_resource:
+    type: OS::Heat::TestResource
+    properties:
+      value: ok
+'''
+
+    def test_events(self):
+        queue_id = str(uuid.uuid4())
+        environment = {'event_sinks': [{'type': 'zaqar-queue',
+                                        'target': queue_id,
+                                        'ttl': 120}]}
+        stack_identifier = self.stack_create(
+            template=self.template,
+            environment=environment)
+        stack_name, stack_id = stack_identifier.split('/')
+        conf = {
+            'auth_opts': {
+                'backend': 'keystone',
+                'options': {
+                    'os_username': self.conf.username,
+                    'os_password': self.conf.password,
+                    'os_project_name': self.conf.tenant_name,
+                    'os_auth_url': self.conf.auth_url
+                }
+            }
+        }
+
+        zaqar = zaqarclient.Client(conf=conf, version=1.1)
+        queue = zaqar.queue(queue_id)
+        messages = list(queue.messages())
+        self.assertEqual(4, len(messages))
+        types = [m.body['type'] for m in messages]
+        self.assertEqual(['os.heat.event'] * 4, types)
+        resources = set([m.body['payload']['resource_name'] for m in messages])
+        self.assertEqual(set([stack_name, 'test_resource']), resources)
+        stack_ids = [m.body['payload']['stack_id'] for m in messages]
+        self.assertEqual([stack_id] * 4, stack_ids)
+        statuses = [m.body['payload']['resource_status'] for m in messages]
+        statuses.sort()
+        self.assertEqual(
+            ['COMPLETE', 'COMPLETE', 'IN_PROGRESS', 'IN_PROGRESS'], statuses)
+        actions = [m.body['payload']['resource_action'] for m in messages]
+        self.assertEqual(['CREATE'] * 4, actions)
diff --git a/functional/test_hooks.py b/functional/test_hooks.py
index d9ebd44..bafb0ef 100644
--- a/functional/test_hooks.py
+++ b/functional/test_hooks.py
@@ -191,7 +191,7 @@
                          res_after.physical_resource_id)
 
     def test_hook_pre_create_nested(self):
-        files = {'nested.yaml': yaml.dump(self.template)}
+        files = {'nested.yaml': yaml.safe_dump(self.template)}
         env = {'resource_registry':
                {'resources':
                 {'nested':
diff --git a/functional/test_immutable_parameters.py b/functional/test_immutable_parameters.py
new file mode 100644
index 0000000..d223b14
--- /dev/null
+++ b/functional/test_immutable_parameters.py
@@ -0,0 +1,141 @@
+#    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
+from heatclient import exc as heat_exceptions
+
+
+class ImmutableParametersTest(functional_base.FunctionalTestsBase):
+
+    template_param_has_no_immutable_field = '''
+heat_template_version: 2014-10-16
+parameters:
+  param1:
+    type: string
+    default: default_value
+outputs:
+  param1_output:
+    description: 'parameter 1 details'
+    value: { get_param: param1 }
+'''
+
+    template_param_has_immutable_field = '''
+heat_template_version: 2014-10-16
+parameters:
+  param1:
+    type: string
+    default: default_value
+    immutable: false
+outputs:
+  param1_output:
+    description: 'parameter 1 details'
+    value: { get_param: param1 }
+'''
+
+    def test_no_immutable_param_field(self):
+        param1_create_value = 'value1'
+        create_parameters = {"param1": param1_create_value}
+
+        stack_identifier = self.stack_create(
+            template=self.template_param_has_no_immutable_field,
+            parameters=create_parameters
+        )
+        stack = self.client.stacks.get(stack_identifier)
+
+        # Verify the value of the parameter
+        self.assertEqual(param1_create_value,
+                         self._stack_output(stack, 'param1_output'))
+
+        param1_update_value = 'value2'
+        update_parameters = {"param1": param1_update_value}
+
+        self.update_stack(
+            stack_identifier,
+            template=self.template_param_has_no_immutable_field,
+            parameters=update_parameters)
+
+        stack = self.client.stacks.get(stack_identifier)
+
+        # Verify the value of the updated parameter
+        self.assertEqual(param1_update_value,
+                         self._stack_output(stack, 'param1_output'))
+
+    def test_immutable_param_field_allowed(self):
+        param1_create_value = 'value1'
+        create_parameters = {"param1": param1_create_value}
+
+        stack_identifier = self.stack_create(
+            template=self.template_param_has_immutable_field,
+            parameters=create_parameters
+        )
+        stack = self.client.stacks.get(stack_identifier)
+
+        # Verify the value of the parameter
+        self.assertEqual(param1_create_value,
+                         self._stack_output(stack, 'param1_output'))
+
+        param1_update_value = 'value2'
+        update_parameters = {"param1": param1_update_value}
+
+        self.update_stack(
+            stack_identifier,
+            template=self.template_param_has_immutable_field,
+            parameters=update_parameters)
+        stack = self.client.stacks.get(stack_identifier)
+
+        # Verify the value of the updated parameter
+        self.assertEqual(param1_update_value,
+                         self._stack_output(stack, 'param1_output'))
+
+        # Ensure stack is not in a failed state
+        self.assertEqual('UPDATE_COMPLETE', stack.stack_status)
+
+    def test_immutable_param_field_error(self):
+        param1_create_value = 'value1'
+        create_parameters = {"param1": param1_create_value}
+
+        # Toggle the immutable field to preclude updating
+        immutable_true = self.template_param_has_immutable_field.replace(
+            'immutable: false', 'immutable: true')
+
+        stack_identifier = self.stack_create(
+            template=immutable_true,
+            parameters=create_parameters
+        )
+        stack = self.client.stacks.get(stack_identifier)
+
+        param1_update_value = 'value2'
+        update_parameters = {"param1": param1_update_value}
+
+        # Verify the value of the parameter
+        self.assertEqual(param1_create_value,
+                         self._stack_output(stack, 'param1_output'))
+
+        # Attempt to update the stack with a new parameter value
+        try:
+            self.update_stack(
+                stack_identifier,
+                template=immutable_true,
+                parameters=update_parameters)
+        except heat_exceptions.HTTPBadRequest as exc:
+            exp = ('The following parameters are immutable and may not be '
+                   'updated: param1')
+            self.assertIn(exp, str(exc))
+
+        stack = self.client.stacks.get(stack_identifier)
+
+        # Ensure stack is not in a failed state
+        self.assertEqual('CREATE_COMPLETE', stack.stack_status)
+
+        # Ensure immutable parameter has not changed
+        self.assertEqual(param1_create_value,
+                         self._stack_output(stack, 'param1_output'))
diff --git a/functional/test_template_resource.py b/functional/test_template_resource.py
index fa34e2e..9a3e833 100644
--- a/functional/test_template_resource.py
+++ b/functional/test_template_resource.py
@@ -848,3 +848,90 @@
             exp = ('ERROR: Attribute here-it-is for facade '
                    'OS::Thingy missing in provider')
             self.assertEqual(exp, six.text_type(exc))
+
+
+class TemplateResourceNewParamTest(functional_base.FunctionalTestsBase):
+
+    main_template = '''
+heat_template_version: 2013-05-23
+resources:
+  my_resource:
+    type: resource.yaml
+    properties:
+      value1: foo
+'''
+    nested_templ = '''
+heat_template_version: 2013-05-23
+parameters:
+  value1:
+    type: string
+resources:
+  test:
+    type: OS::Heat::TestResource
+    properties:
+      value: {get_param: value1}
+'''
+    main_template_update = '''
+heat_template_version: 2013-05-23
+resources:
+  my_resource:
+    type: resource.yaml
+    properties:
+      value1: foo
+      value2: foo
+'''
+    nested_templ_update_fail = '''
+heat_template_version: 2013-05-23
+parameters:
+  value1:
+    type: string
+  value2:
+    type: string
+resources:
+  test:
+    type: OS::Heat::TestResource
+    properties:
+      fail: True
+      value:
+        str_replace:
+          template: VAL1-VAL2
+          params:
+            VAL1: {get_param: value1}
+            VAL2: {get_param: value2}
+'''
+    nested_templ_update = '''
+heat_template_version: 2013-05-23
+parameters:
+  value1:
+    type: string
+  value2:
+    type: string
+resources:
+  test:
+    type: OS::Heat::TestResource
+    properties:
+      value:
+        str_replace:
+          template: VAL1-VAL2
+          params:
+            VAL1: {get_param: value1}
+            VAL2: {get_param: value2}
+'''
+
+    def test_update(self):
+        stack_identifier = self.stack_create(
+            template=self.main_template,
+            files={'resource.yaml': self.nested_templ})
+
+        # Make the update fails with the new parameter inserted.
+        self.update_stack(
+            stack_identifier,
+            self.main_template_update,
+            files={'resource.yaml': self.nested_templ_update_fail},
+            expected_status='UPDATE_FAILED')
+
+        # Fix the update, it should succeed now.
+        self.update_stack(
+            stack_identifier,
+            self.main_template_update,
+            files={'resource.yaml': self.nested_templ_update})
diff --git a/functional/test_update_restricted.py b/functional/test_update_restricted.py
new file mode 100644
index 0000000..ae1907b
--- /dev/null
+++ b/functional/test_update_restricted.py
@@ -0,0 +1,155 @@
+#    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
+
+test_template = {
+    'heat_template_version': '2013-05-23',
+    'description': 'Test template to create one instance.',
+    'resources': {
+        'bar': {
+            'type': 'OS::Heat::TestResource',
+            'properties': {
+                'value': '1234',
+                'update_replace': False,
+            }
+        }
+    }
+}
+
+env_both_restrict = {u'resource_registry': {
+    u'resources': {
+        'bar': {'restricted_actions': ['update', 'replace']}
+    }
+}
+}
+
+env_replace_restrict = {u'resource_registry': {
+    u'resources': {
+        '*ar': {'restricted_actions': 'replace'}
+    }
+}
+}
+
+reason_update_restrict = 'update is restricted for resource.'
+reason_replace_restrict = 'replace is restricted for resource.'
+
+
+class UpdateRestrictedStackTest(functional_base.FunctionalTestsBase):
+
+    def _check_for_restriction_reason(self, events,
+                                      reason, num_expected=1):
+        matched = [e for e in events
+                   if e.resource_status_reason == reason]
+        return len(matched) == num_expected
+
+    def test_update(self):
+        stack_identifier = self.stack_create(template=test_template)
+
+        update_template = test_template.copy()
+        props = update_template['resources']['bar']['properties']
+        props['value'] = '4567'
+
+        # check update fails - with 'both' restricted
+        self.update_stack(stack_identifier, update_template,
+                          env_both_restrict,
+                          expected_status='UPDATE_FAILED')
+
+        self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+                                                    'CREATE_COMPLETE'))
+        resource_events = self.client.events.list(stack_identifier, 'bar')
+        self.assertTrue(
+            self._check_for_restriction_reason(resource_events,
+                                               reason_update_restrict))
+
+        # check update succeeds - with only 'replace' restricted
+        self.update_stack(stack_identifier, update_template,
+                          env_replace_restrict,
+                          expected_status='UPDATE_COMPLETE')
+
+        self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+                                                    'UPDATE_COMPLETE'))
+        resource_events = self.client.events.list(stack_identifier, 'bar')
+        self.assertFalse(
+            self._check_for_restriction_reason(resource_events,
+                                               reason_update_restrict, 2))
+        self.assertTrue(
+            self._check_for_restriction_reason(resource_events,
+                                               reason_replace_restrict, 0))
+
+    def test_replace(self):
+        stack_identifier = self.stack_create(template=test_template)
+
+        update_template = test_template.copy()
+        props = update_template['resources']['bar']['properties']
+        props['update_replace'] = True
+
+        # check replace fails - with 'both' restricted
+        self.update_stack(stack_identifier, update_template,
+                          env_replace_restrict,
+                          expected_status='UPDATE_FAILED')
+
+        self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+                                                    'CREATE_COMPLETE'))
+        resource_events = self.client.events.list(stack_identifier, 'bar')
+        self.assertTrue(
+            self._check_for_restriction_reason(resource_events,
+                                               reason_replace_restrict))
+
+        # check replace fails - with only 'replace' restricted
+        self.update_stack(stack_identifier, update_template,
+                          env_replace_restrict,
+                          expected_status='UPDATE_FAILED')
+
+        self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+                                                    'CREATE_COMPLETE'))
+        resource_events = self.client.events.list(stack_identifier, 'bar')
+        self.assertTrue(
+            self._check_for_restriction_reason(resource_events,
+                                               reason_replace_restrict, 2))
+        self.assertTrue(
+            self._check_for_restriction_reason(resource_events,
+                                               reason_update_restrict, 0))
+
+    def test_update_type_changed(self):
+        stack_identifier = self.stack_create(template=test_template)
+
+        update_template = test_template.copy()
+        rsrc = update_template['resources']['bar']
+        rsrc['type'] = 'OS::Heat::None'
+
+        # check replace fails - with 'both' restricted
+        self.update_stack(stack_identifier, update_template,
+                          env_both_restrict,
+                          expected_status='UPDATE_FAILED')
+
+        self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+                                                    'CREATE_COMPLETE'))
+        resource_events = self.client.events.list(stack_identifier, 'bar')
+        self.assertTrue(
+            self._check_for_restriction_reason(resource_events,
+                                               reason_replace_restrict))
+
+        # check replace fails - with only 'replace' restricted
+        self.update_stack(stack_identifier, update_template,
+                          env_replace_restrict,
+                          expected_status='UPDATE_FAILED')
+
+        self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+                                                    'CREATE_COMPLETE'))
+        resource_events = self.client.events.list(stack_identifier, 'bar')
+        self.assertTrue(
+            self._check_for_restriction_reason(resource_events,
+                                               reason_replace_restrict, 2))
+        self.assertTrue(
+            self._check_for_restriction_reason(resource_events,
+                                               reason_update_restrict, 0))