Merge "Test case for resource_facade"
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 1039625..21c5ba1 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/common/test_resources/test_resource.py b/common/test_resources/test_resource.py
deleted file mode 100644
index 55255a6..0000000
--- a/common/test_resources/test_resource.py
+++ /dev/null
@@ -1,134 +0,0 @@
-#
-#    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 eventlet
-
-from heat.common.i18n import _
-from heat.engine import attributes
-from heat.engine import properties
-from heat.engine import resource
-from heat.engine import support
-from oslo_log import log as logging
-
-LOG = logging.getLogger(__name__)
-
-
-class TestResource(resource.Resource):
-    '''
-    A resource which stores the string value that was provided.
-
-    This resource is to be used only for testing.
-    It has control knobs such as 'update_replace', 'fail', 'wait_secs'
-
-    '''
-
-    support_status = support.SupportStatus(version='2014.1')
-
-    PROPERTIES = (
-        VALUE, UPDATE_REPLACE, FAIL, WAIT_SECS
-    ) = (
-        'value', 'update_replace', 'fail', 'wait_secs'
-    )
-
-    ATTRIBUTES = (
-        OUTPUT,
-    ) = (
-        'output',
-    )
-
-    properties_schema = {
-        VALUE: properties.Schema(
-            properties.Schema.STRING,
-            _('The input string to be stored.'),
-            default='test_string',
-            update_allowed=True
-        ),
-        FAIL: properties.Schema(
-            properties.Schema.BOOLEAN,
-            _('Value which can be set to fail the resource operation '
-              'to test failure scenarios.'),
-            update_allowed=True,
-            default=False
-        ),
-        UPDATE_REPLACE: properties.Schema(
-            properties.Schema.BOOLEAN,
-            _('Value which can be set to trigger update replace for '
-              'the particular resource'),
-            update_allowed=True,
-            default=False
-        ),
-        WAIT_SECS: properties.Schema(
-            properties.Schema.NUMBER,
-            _('Value which can be set for resource to wait after an action '
-              'is performed.'),
-            update_allowed=True,
-            default=0,
-        ),
-    }
-
-    attributes_schema = {
-        OUTPUT: attributes.Schema(
-            _('The string that was stored. This value is '
-              'also available by referencing the resource.'),
-            cache_mode=attributes.Schema.CACHE_NONE
-        ),
-    }
-
-    def handle_create(self):
-        value = self.properties.get(self.VALUE)
-        fail_prop = self.properties.get(self.FAIL)
-        sleep_secs = self.properties.get(self.WAIT_SECS)
-
-        self.data_set('value', value, redact=False)
-        self.resource_id_set(self.physical_resource_name())
-
-        # sleep for specified time
-        if sleep_secs:
-            LOG.debug("Resource %s sleeping for %s seconds",
-                      self.name, sleep_secs)
-            eventlet.sleep(sleep_secs)
-
-        # emulate failure
-        if fail_prop:
-            raise ValueError("Test Resource failed %s" % self.name)
-
-    def handle_update(self, json_snippet=None, tmpl_diff=None, prop_diff=None):
-        value = prop_diff.get(self.VALUE)
-        new_prop = json_snippet._properties
-        if value:
-            update_replace = new_prop.get(self.UPDATE_REPLACE, False)
-            if update_replace:
-                raise resource.UpdateReplace(self.name)
-            else:
-                fail_prop = new_prop.get(self.FAIL, False)
-                sleep_secs = new_prop.get(self.WAIT_SECS, 0)
-                # emulate failure
-                if fail_prop:
-                    raise Exception("Test Resource failed %s", self.name)
-                # update in place
-                self.data_set('value', value, redact=False)
-
-                if sleep_secs:
-                    LOG.debug("Update of Resource %s sleeping for %s seconds",
-                              self.name, sleep_secs)
-                    eventlet.sleep(sleep_secs)
-
-    def _resolve_attribute(self, name):
-        if name == self.OUTPUT:
-            return self.data().get('value')
-
-
-def resource_mapping():
-    return {
-        'OS::Heat::TestResource': TestResource,
-    }
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 87d7a23..4597d4c 100644
--- a/functional/test_create_update.py
+++ b/functional/test_create_update.py
@@ -26,7 +26,10 @@
                 'value': 'Test1',
                 'fail': False,
                 'update_replace': False,
-                'wait_secs': 0
+                'wait_secs': 0,
+                'action_wait_secs': {'create': 1},
+                'client_name': 'nova',
+                'entity_name': 'servers',
             }
         }
     }
@@ -42,7 +45,8 @@
                 'value': 'Test1',
                 'fail': False,
                 'update_replace': False,
-                'wait_secs': 0
+                'wait_secs': 0,
+                'action_wait_secs': {'update': 1}
             }
         },
         'test2': {
@@ -98,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
 '''
@@ -130,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()
 
@@ -239,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'],
@@ -290,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'],
@@ -387,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_software_config.py b/functional/test_software_config.py
index 019e6c9..af7d671 100644
--- a/functional/test_software_config.py
+++ b/functional/test_software_config.py
@@ -83,7 +83,7 @@
             2,
             5,
             deploy_count)
-        deploy_count = self.deploy_many_configs(
+        self.deploy_many_configs(
             stack_identifier,
             server,
             config_stacks,
@@ -95,24 +95,15 @@
         for config_stack in config_stacks:
             self._wait_for_stack_status(config_stack, 'CREATE_COMPLETE')
 
-    def deploy_many_configs(self, stack_identifier, server, config_stacks,
+    def deploy_many_configs(self, stack, server, config_stacks,
                             stack_count, deploys_per_stack,
                             deploy_count_start):
         for a in range(stack_count):
             config_stacks.append(
                 self.deploy_config(server, deploys_per_stack))
 
-        for config_stack in config_stacks:
-            self.wait_for_deploy_physical_id(config_stack)
-
         new_count = deploy_count_start + stack_count * deploys_per_stack
-        server_metadata = self.client.resources.metadata(
-            stack_identifier, 'server')
-        self.assertEqual(
-            new_count,
-            len(server_metadata['deployments']),
-            '%s stacks with %s deployments' % (stack_count, deploys_per_stack)
-        )
+        self.wait_for_deploy_metadata_set(stack, new_count)
         return new_count
 
     def deploy_config(self, server, deploy_count):
@@ -128,19 +119,16 @@
             enable_cleanup=self.enable_cleanup,
             expected_status=None)
 
-    def wait_for_deploy_physical_id(self, stack):
+    def wait_for_deploy_metadata_set(self, stack, deploy_count):
         build_timeout = self.conf.build_timeout
         build_interval = self.conf.build_interval
 
         start = timeutils.utcnow()
         while timeutils.delta_seconds(start,
                                       timeutils.utcnow()) < build_timeout:
-            created = True
-            for res in self.client.resources.list(stack, nested_depth='2'):
-                if not res.physical_resource_id:
-                    created = False
-                    break
-            if created:
+            server_metadata = self.client.resources.metadata(
+                stack, 'server')
+            if len(server_metadata['deployments']) == deploy_count:
                 return
             time.sleep(build_interval)
 
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 6ece7d1..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
@@ -749,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)
diff --git a/scenario/templates/app_server_neutron.yaml b/scenario/templates/app_server_neutron.yaml
new file mode 100644
index 0000000..9cbf82a
--- /dev/null
+++ b/scenario/templates/app_server_neutron.yaml
@@ -0,0 +1,65 @@
+heat_template_version: 2015-10-15
+
+description: |
+  App server that is a member of Neutron Pool.
+
+parameters:
+
+  image:
+    type: string
+
+  flavor:
+    type: string
+
+  net:
+    type: string
+
+  sec_group:
+    type: string
+
+  pool_id:
+    type: string
+
+  app_port:
+    type: number
+
+  timeout:
+    type: number
+
+resources:
+
+  config:
+    type: OS::Test::WebAppConfig
+    properties:
+      app_port: { get_param: app_port }
+      wc_curl_cli: { get_attr: [ handle, curl_cli ] }
+
+  server:
+    type: OS::Nova::Server
+    properties:
+      image: { get_param: image }
+      flavor: { get_param: flavor }
+      networks:
+        - network: { get_param: net }
+      security_groups:
+        - { get_param: sec_group }
+      user_data_format: RAW
+      user_data: { get_resource: config }
+
+  handle:
+    type: OS::Heat::WaitConditionHandle
+
+  waiter:
+    type: OS::Heat::WaitCondition
+    depends_on: server
+    properties:
+      timeout: { get_param: timeout }
+      handle: { get_resource: handle }
+
+  pool_member:
+    type: OS::Neutron::PoolMember
+    depends_on: waiter
+    properties:
+      address: { get_attr: [ server, networks, { get_param: net }, 0 ] }
+      pool_id: { get_param: pool_id }
+      protocol_port: { get_param: app_port }
diff --git a/scenario/templates/netcat-webapp.yaml b/scenario/templates/netcat-webapp.yaml
new file mode 100644
index 0000000..fdb0335
--- /dev/null
+++ b/scenario/templates/netcat-webapp.yaml
@@ -0,0 +1,35 @@
+heat_template_version: 2015-10-15
+
+description: |
+  Simplest web-app using netcat reporting only hostname.
+  Specifically tailored for minimal Cirros image.
+
+parameters:
+
+  app_port:
+    type: number
+
+  wc_curl_cli:
+    type: string
+
+resources:
+
+  webapp_nc:
+    type: OS::Heat::SoftwareConfig
+    properties:
+      group: ungrouped
+      config:
+        str_replace:
+          template: |
+            #! /bin/sh -v
+            Body=$(hostname)
+            Response="HTTP/1.1 200 OK\r\nContent-Length: ${#Body}\r\n\r\n$Body"
+            wc_notify --data-binary '{"status": "SUCCESS"}'
+            while true ; do echo -e $Response | nc -llp PORT; done
+          params:
+            PORT: { get_param: app_port }
+            wc_notify: { get_param: wc_curl_cli }
+
+outputs:
+  OS::stack_id:
+    value: { get_resource: webapp_nc }
diff --git a/scenario/templates/test_autoscaling_lb_neutron.yaml b/scenario/templates/test_autoscaling_lb_neutron.yaml
new file mode 100644
index 0000000..d47e787
--- /dev/null
+++ b/scenario/templates/test_autoscaling_lb_neutron.yaml
@@ -0,0 +1,113 @@
+heat_template_version: 2015-04-30
+
+description: |
+  Template which tests Neutron load balancing requests to members of
+  Heat AutoScalingGroup.
+  Instances must be running some webserver on a given app_port
+  producing HTTP response that is different between servers
+  but stable over time for given server.
+
+parameters:
+  flavor:
+    type: string
+  image:
+    type: string
+  net:
+    type: string
+  subnet:
+    type: string
+  public_net:
+    type: string
+  app_port:
+    type: number
+    default: 8080
+  lb_port:
+    type: number
+    default: 80
+  timeout:
+    type: number
+    default: 600
+
+resources:
+
+  sec_group:
+    type: OS::Neutron::SecurityGroup
+    properties:
+      rules:
+      - remote_ip_prefix: 0.0.0.0/0
+        protocol: tcp
+        port_range_min: { get_param: app_port }
+        port_range_max: { get_param: app_port }
+
+  asg:
+    type: OS::Heat::AutoScalingGroup
+    properties:
+      desired_capacity: 1
+      max_size: 2
+      min_size: 1
+      resource:
+        type: OS::Test::NeutronAppServer
+        properties:
+          image: { get_param: image }
+          flavor: { get_param: flavor }
+          net: { get_param: net}
+          sec_group: { get_resource: sec_group }
+          app_port: { get_param: app_port }
+          pool_id: { get_resource: pool }
+          timeout: { get_param: timeout }
+
+  scale_up:
+    type: OS::Heat::ScalingPolicy
+    properties:
+      adjustment_type: change_in_capacity
+      auto_scaling_group_id: { get_resource: asg }
+      scaling_adjustment: 1
+
+  scale_down:
+    type: OS::Heat::ScalingPolicy
+    properties:
+      adjustment_type: change_in_capacity
+      auto_scaling_group_id: { get_resource: asg }
+      scaling_adjustment: -1
+
+  health_monitor:
+    type: OS::Neutron::HealthMonitor
+    properties:
+      delay: 3
+      type: HTTP
+      timeout: 3
+      max_retries: 3
+
+  pool:
+    type: OS::Neutron::Pool
+    properties:
+      lb_method: ROUND_ROBIN
+      protocol: HTTP
+      subnet: { get_param: subnet }
+      monitors:
+      - { get_resource: health_monitor }
+      vip:
+        protocol_port: { get_param: lb_port }
+
+  floating_ip:
+     type: OS::Neutron::FloatingIP
+     properties:
+       floating_network: { get_param: public_net }
+       port_id:
+         { get_attr: [pool, vip, 'port_id'] }
+
+  loadbalancer:
+    type: OS::Neutron::LoadBalancer
+    properties:
+      pool_id: { get_resource: pool }
+      protocol_port: { get_param: app_port }
+
+outputs:
+  lburl:
+    description: URL of the loadbalanced app
+    value:
+      str_replace:
+        template: http://IP_ADDRESS:PORT
+        params:
+          IP_ADDRESS: { get_attr: [ floating_ip, floating_ip_address ] }
+          PORT: { get_param: lb_port }
diff --git a/scenario/templates/test_neutron_autoscaling.yaml b/scenario/templates/test_neutron_autoscaling.yaml
deleted file mode 100644
index a34ec43..0000000
--- a/scenario/templates/test_neutron_autoscaling.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-heat_template_version: 2014-10-16
-
-description: Auto-scaling Test
-
-parameters:
-  image_id:
-    type: string
-    label: Image ID
-    description: Image ID from configurations
-  capacity:
-    type: string
-    label: Capacity
-    description: Auto-scaling group desired capacity
-  fixed_subnet:
-    type: string
-    label: fixed subnetwork ID
-    description: subnetwork ID used for autoscaling
-  instance_type:
-    type: string
-    label: instance_type
-    description: type of instance to launch
-
-resources:
-  test_pool:
-    type: OS::Neutron::Pool
-    properties:
-      description: Test Pool
-      lb_method: ROUND_ROBIN
-      name: test_pool
-      protocol: HTTP
-      subnet: { get_param: fixed_subnet }
-      vip: {
-        "description": "Test VIP",
-        "protocol_port": 80,
-        "name": "test_vip"
-      }
-  load_balancer:
-    type: OS::Neutron::LoadBalancer
-    properties:
-      protocol_port: 80
-      pool_id: { get_resource: test_pool }
-  launch_config:
-    type: AWS::AutoScaling::LaunchConfiguration
-    properties:
-      ImageId: { get_param: image_id }
-      InstanceType: { get_param: instance_type }
-  server_group:
-    type: AWS::AutoScaling::AutoScalingGroup
-    properties:
-      AvailabilityZones : ["nova"]
-      LaunchConfigurationName : { get_resource : launch_config }
-      VPCZoneIdentifier: [{ get_param: fixed_subnet }]
-      MinSize : 1
-      MaxSize : 5
-      DesiredCapacity: { get_param: capacity }
-      LoadBalancerNames : [ { get_resource : load_balancer } ]
diff --git a/scenario/templates/test_neutron_loadbalancer.yaml b/scenario/templates/test_neutron_loadbalancer.yaml
deleted file mode 100644
index dd659d0..0000000
--- a/scenario/templates/test_neutron_loadbalancer.yaml
+++ /dev/null
@@ -1,133 +0,0 @@
-heat_template_version: 2014-10-16
-
-description: |
-  Template which tests neutron load balancing resources
-
-parameters:
-  key_name:
-    type: string
-  flavor:
-    type: string
-  image:
-    type: string
-  network:
-    type: string
-  private_subnet_id:
-    type: string
-  external_network_id:
-    type: string
-  port:
-    type: string
-    default: '80'
-  timeout:
-    type: number
-
-resources:
-  sec_group:
-    type: OS::Neutron::SecurityGroup
-    properties:
-      description: Add security group rules for servers
-      name: security-group
-      rules:
-        - remote_ip_prefix: 0.0.0.0/0
-          protocol: tcp
-          port_range_min: { get_param: port }
-          port_range_max: { get_param: port }
-        - remote_ip_prefix: 0.0.0.0/0
-          protocol: icmp
-
-  wait_condition:
-    type: OS::Heat::WaitCondition
-    properties:
-      handle: { get_resource: wait_condition_handle }
-      count: 2
-      timeout: { get_param: timeout }
-
-  wait_condition_handle:
-    type: OS::Heat::WaitConditionHandle
-
-  config:
-    type: OS::Heat::SoftwareConfig
-    properties:
-      group: ungrouped
-      config:
-        str_replace:
-          template: |
-            #!/bin/bash -v
-            echo  $(hostname) > index.html
-            python -m SimpleHTTPServer port &
-            wc_notify --data-binary '{"status": "SUCCESS"}'
-          params:
-            wc_notify: { get_attr: ['wait_condition_handle', 'curl_cli'] }
-            port: { get_param: port }
-
-  server1:
-    type: OS::Nova::Server
-    properties:
-      name: Server1
-      image: { get_param: image }
-      flavor: { get_param: flavor }
-      key_name: { get_param: key_name }
-      networks: [{network: {get_param: network} }]
-      security_groups: [{ get_resource: sec_group }]
-      user_data_format: SOFTWARE_CONFIG
-      user_data: { get_resource: config }
-
-  server2:
-    type: OS::Nova::Server
-    properties:
-      name: Server2
-      image: { get_param: image }
-      flavor: { get_param: flavor }
-      key_name: { get_param: key_name }
-      networks: [{network: {get_param: network} }]
-      security_groups: [{ get_resource: sec_group }]
-      user_data_format: SOFTWARE_CONFIG
-      user_data: { get_resource: config }
-
-  health_monitor:
-    type: OS::Neutron::HealthMonitor
-    properties:
-      delay: 3
-      type: HTTP
-      timeout: 3
-      max_retries: 3
-
-  test_pool:
-    type: OS::Neutron::Pool
-    properties:
-      lb_method: ROUND_ROBIN
-      protocol: HTTP
-      subnet: { get_param: private_subnet_id }
-      monitors:
-      - { get_resource: health_monitor }
-      vip:
-        protocol_port: { get_param: port }
-
-  floating_ip:
-     type: OS::Neutron::FloatingIP
-     properties:
-       floating_network: { get_param: external_network_id }
-       port_id:
-         { get_attr: [test_pool, vip, 'port_id'] }
-       fixed_ip_address:
-         { get_attr: [test_pool, vip, 'address'] }
-
-  LBaaS:
-    type: OS::Neutron::LoadBalancer
-    depends_on: wait_condition
-    properties:
-      pool_id: { get_resource: test_pool }
-      protocol_port: { get_param: port }
-      members:
-      - { get_resource: server1 }
-
-outputs:
-  serv1_ip:
-    value: {get_attr: [server1, networks, { get_param: network }, 0]}
-  serv2_ip:
-    value: {get_attr: [server2, networks, { get_param: network }, 0]}
-  vip:
-    value: {get_attr: [test_pool, vip, address]}
-  fip:
-    value: {get_attr: [floating_ip, floating_ip_address]}
diff --git a/scenario/test_autoscaling_lb.py b/scenario/test_autoscaling_lb.py
new file mode 100644
index 0000000..21b27dd
--- /dev/null
+++ b/scenario/test_autoscaling_lb.py
@@ -0,0 +1,114 @@
+#
+# 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 time
+
+import requests
+
+from heat_integrationtests.common import test
+from heat_integrationtests.scenario import scenario_base
+
+
+class AutoscalingLoadBalancerTest(scenario_base.ScenarioTestsBase):
+    """
+    The class is responsible for testing ASG + LB scenario.
+
+    The very common use case tested is an autoscaling group
+    of some web application servers behind a loadbalancer.
+    """
+
+    def setUp(self):
+        super(AutoscalingLoadBalancerTest, self).setUp()
+        self.template_name = 'test_autoscaling_lb_neutron.yaml'
+        self.app_server_template_name = 'app_server_neutron.yaml'
+        self.webapp_template_name = 'netcat-webapp.yaml'
+
+    def check_num_responses(self, url, expected_num, retries=10):
+        resp = set()
+        for count in range(retries):
+            time.sleep(1)
+            r = requests.get(url)
+            # skip unsuccessfull requests
+            if r.status_code == 200:
+                resp.add(r.text)
+        self.assertEqual(expected_num, len(resp))
+
+    def autoscale_complete(self, stack_id, expected_num):
+        res_list = self.client.resources.list(stack_id)
+        all_res_complete = all(res.resource_status in ('UPDATE_COMPLETE',
+                                                       'CREATE_COMPLETE')
+                               for res in res_list)
+        all_res = len(res_list) == expected_num
+        return all_res and all_res_complete
+
+    def test_autoscaling_loadbalancer_neutron(self):
+        """
+        Check work of AutoScaing and Neutron LBaaS resource in Heat.
+
+        The scenario is the following:
+            1. Launch a stack with a load balancer and autoscaling group
+               of one server, wait until stack create is complete.
+            2. Check that there is only one distinctive response from
+               loadbalanced IP.
+            3. Signal the scale_up policy, wait until all resources in
+               autoscaling group are complete.
+            4. Check that now there are two distinctive responses from
+               loadbalanced IP.
+        """
+
+        parameters = {
+            'flavor': self.conf.minimal_instance_type,
+            'image': self.conf.minimal_image_ref,
+            'net': self.conf.fixed_network_name,
+            'subnet': self.conf.fixed_subnet_name,
+            'public_net': self.conf.floating_network_name,
+            'app_port': 8080,
+            'lb_port': 80,
+            'timeout': 600
+        }
+
+        app_server_template = self._load_template(
+            __file__, self.app_server_template_name, self.sub_dir
+        )
+        webapp_template = self._load_template(
+            __file__, self.webapp_template_name, self.sub_dir
+        )
+        files = {'appserver.yaml': app_server_template,
+                 'webapp.yaml': webapp_template}
+        env = {'resource_registry':
+               {'OS::Test::NeutronAppServer': 'appserver.yaml',
+                'OS::Test::WebAppConfig': 'webapp.yaml'}}
+        # Launch stack
+        sid = self.launch_stack(
+            template_name=self.template_name,
+            parameters=parameters,
+            files=files,
+            environment=env
+        )
+        stack = self.client.stacks.get(sid)
+        lb_url = self._stack_output(stack, 'lburl')
+        # Check number of distinctive responces, must be 1
+        self.check_num_responses(lb_url, 1)
+
+        # Signal the scaling hook
+        self.client.resources.signal(sid, 'scale_up')
+
+        # Wait for AutoScalingGroup update to finish
+        asg = self.client.resources.get(sid, 'asg')
+        test.call_until_true(self.conf.build_timeout,
+                             self.conf.build_interval,
+                             self.autoscale_complete,
+                             asg.physical_resource_id, 2)
+
+        # Check number of distinctive responses, must now be 2
+        self.check_num_responses(lb_url, 2)
diff --git a/scenario/test_neutron_autoscaling.py b/scenario/test_neutron_autoscaling.py
deleted file mode 100644
index e7aae19..0000000
--- a/scenario/test_neutron_autoscaling.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# 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.scenario import scenario_base
-
-
-class NeutronAutoscalingTest(scenario_base.ScenarioTestsBase):
-    """
-    The class is responsible for testing of neutron resources autoscaling.
-    """
-
-    def setUp(self):
-        super(NeutronAutoscalingTest, self).setUp()
-        if not self.conf.fixed_subnet_name:
-            raise self.skipException("No sub-network configured to test")
-        self.template_name = 'test_neutron_autoscaling.yaml'
-
-    def test_neutron_autoscaling(self):
-        """
-        Check autoscaling of load balancer members in Heat.
-
-        The alternative scenario is the following:
-            1. Launch a stack with a load balancer.
-            2. Check that the load balancer created
-            one load balancer member for stack.
-            3. Update stack definition: increase desired capacity of stack.
-            4. Check that number of members in load balancer was increased.
-        """
-
-        parameters = {
-            "image_id": self.conf.minimal_image_ref,
-            "capacity": "1",
-            "instance_type": self.conf.minimal_instance_type,
-            "fixed_subnet": self.net['subnets'][0],
-        }
-
-        # Launch stack
-        stack_id = self.launch_stack(
-            template_name=self.template_name,
-            parameters=parameters
-        )
-
-        # Check number of members
-        pool_resource = self.client.resources.get(stack_id, 'test_pool')
-        pool_members = self.network_client.list_members(
-            pool_id=pool_resource.physical_resource_id)['members']
-        self.assertEqual(1, len(pool_members))
-
-        # Increase desired capacity and update the stack
-        template = self._load_template(
-            __file__, self.template_name, self.sub_dir
-        )
-        parameters["capacity"] = "2"
-        self.update_stack(
-            stack_id,
-            template=template,
-            parameters=parameters
-        )
-
-        # Check number of members
-        pool_members = self.network_client.list_members(
-            pool_id=pool_resource.physical_resource_id)['members']
-        self.assertEqual(2, len(pool_members))
diff --git a/scenario/test_neutron_loadbalancer.py b/scenario/test_neutron_loadbalancer.py
deleted file mode 100644
index d8e0197..0000000
--- a/scenario/test_neutron_loadbalancer.py
+++ /dev/null
@@ -1,108 +0,0 @@
-#
-# 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 time
-
-from six.moves import urllib
-
-from heat_integrationtests.scenario import scenario_base
-
-
-class NeutronLoadBalancerTest(scenario_base.ScenarioTestsBase):
-    """
-    The class is responsible for testing of neutron resources balancer.
-    """
-
-    def setUp(self):
-        super(NeutronLoadBalancerTest, self).setUp()
-        self.public_net = self._get_network(self.conf.floating_network_name)
-        self.template_name = 'test_neutron_loadbalancer.yaml'
-
-    def collect_responses(self, ip, expected_resp):
-        resp = set()
-        for count in range(10):
-            time.sleep(1)
-            resp.add(urllib.request.urlopen('http://%s/' % ip).read())
-
-        self.assertEqual(expected_resp, resp)
-
-    def test_neutron_loadbalancer(self):
-        """
-        Check work of Neutron LBaaS resource in Heat.
-
-        The alternative scenario is the following:
-            1. Launch a stack with a load balancer, two servers,
-               but use only one as a LB member.
-            2. Check connection to the servers and LB.
-            3. Collect info about responces, which were received by LB from
-               its members (responces have to be received only from 'server1').
-            4. Update stack definition: include 'server2' into LBaaS.
-            5. Check that number of members in LB was increased and
-               responces were received from 'server1' and 'server2'.
-        """
-
-        parameters = {
-            'key_name': self.keypair_name,
-            'flavor': self.conf.minimal_instance_type,
-            'image': self.conf.image_ref,
-            'network': self.net['name'],
-            'private_subnet_id': self.net['subnets'][0],
-            'external_network_id': self.public_net['id'],
-            'timeout': self.conf.build_timeout
-        }
-
-        # Launch stack
-        sid = self.launch_stack(
-            template_name=self.template_name,
-            parameters=parameters
-        )
-
-        stack = self.client.stacks.get(sid)
-        floating_ip = self._stack_output(stack, 'fip')
-        vip = self._stack_output(stack, 'vip')
-        server1_ip = self._stack_output(stack, 'serv1_ip')
-        server2_ip = self._stack_output(stack, 'serv2_ip')
-        # Check connection and info about received responses
-        self.check_connectivity(server1_ip)
-        self.collect_responses(server1_ip, {'server1\n'})
-
-        self.check_connectivity(server2_ip)
-        self.collect_responses(server2_ip, {'server2\n'})
-
-        self.check_connectivity(vip)
-        self.collect_responses(vip, {'server1\n'})
-
-        self.check_connectivity(floating_ip)
-        self.collect_responses(floating_ip, {'server1\n'})
-
-        # Include 'server2' to LB and update the stack
-        template = self._load_template(
-            __file__, self.template_name, self.sub_dir
-        )
-
-        template = template.replace(
-            '- { get_resource: server1 }',
-            '- { get_resource: server1 }\n      - { get_resource: server2 }\n'
-        )
-
-        self.update_stack(
-            sid,
-            template=template,
-            parameters=parameters
-        )
-
-        self.check_connectivity(vip)
-        self.collect_responses(vip, {'server1\n', 'server2\n'})
-
-        self.check_connectivity(floating_ip)
-        self.collect_responses(floating_ip, {'server1\n', 'server2\n'})