Merge "Skip Stack Adopt/Abandon integration tests when Stack Adopt is disabled"
diff --git a/common/clients.py b/common/clients.py
index 965b6ab..5a4dd5a 100644
--- a/common/clients.py
+++ b/common/clients.py
@@ -18,6 +18,7 @@
 import keystoneclient.v2_0.client
 import neutronclient.v2_0.client
 import novaclient.client
+import swiftclient
 
 import logging
 
@@ -41,6 +42,7 @@
         self.compute_client = self._get_compute_client()
         self.network_client = self._get_network_client()
         self.volume_client = self._get_volume_client()
+        self.object_client = self._get_object_client()
 
     def _get_orchestration_client(self):
         region = self.conf.region
@@ -125,3 +127,16 @@
             endpoint_type=endpoint_type,
             insecure=dscv,
             http_log_debug=True)
+
+    def _get_object_client(self):
+        dscv = self.conf.disable_ssl_certificate_validation
+        args = {
+            'auth_version': '2.0',
+            'tenant_name': self.conf.tenant_name,
+            'user': self.conf.username,
+            'key': self.conf.password,
+            'authurl': self.conf.auth_url,
+            'os_options': {'endpoint_type': 'publicURL'},
+            'insecure': dscv,
+        }
+        return swiftclient.client.Connection(**args)
diff --git a/common/test.py b/common/test.py
index 60ed393..b4c3c81 100644
--- a/common/test.py
+++ b/common/test.py
@@ -33,7 +33,7 @@
 _LOG_FORMAT = "%(levelname)8s [%(name)s] %(message)s"
 
 
-def call_until_true(func, duration, sleep_for):
+def call_until_true(duration, sleep_for, func, *args, **kwargs):
     """
     Call the given function until it returns True (and return True) or
     until the specified duration (in seconds) elapses (and return
@@ -48,7 +48,7 @@
     now = time.time()
     timeout = now + duration
     while now < timeout:
-        if func():
+        if func(*args, **kwargs):
             return True
         LOG.debug("Sleeping for %d seconds", sleep_for)
         time.sleep(sleep_for)
@@ -85,73 +85,9 @@
         self.compute_client = self.manager.compute_client
         self.network_client = self.manager.network_client
         self.volume_client = self.manager.volume_client
+        self.object_client = self.manager.object_client
         self.useFixture(fixtures.FakeLogger(format=_LOG_FORMAT))
 
-    def status_timeout(self, things, thing_id, expected_status,
-                       error_status='ERROR',
-                       not_found_exception=heat_exceptions.NotFound):
-        """
-        Given a thing and an expected status, do a loop, sleeping
-        for a configurable amount of time, checking for the
-        expected status to show. At any time, if the returned
-        status of the thing is ERROR, fail out.
-        """
-        self._status_timeout(things, thing_id,
-                             expected_status=expected_status,
-                             error_status=error_status,
-                             not_found_exception=not_found_exception)
-
-    def _status_timeout(self,
-                        things,
-                        thing_id,
-                        expected_status=None,
-                        allow_notfound=False,
-                        error_status='ERROR',
-                        not_found_exception=heat_exceptions.NotFound):
-
-        log_status = expected_status if expected_status else ''
-        if allow_notfound:
-            log_status += ' or NotFound' if log_status != '' else 'NotFound'
-
-        def check_status():
-            # python-novaclient has resources available to its client
-            # that all implement a get() method taking an identifier
-            # for the singular resource to retrieve.
-            try:
-                thing = things.get(thing_id)
-            except not_found_exception:
-                if allow_notfound:
-                    return True
-                raise
-            except Exception as e:
-                if allow_notfound and self.not_found_exception(e):
-                    return True
-                raise
-
-            new_status = thing.status
-
-            # Some components are reporting error status in lower case
-            # so case sensitive comparisons can really mess things
-            # up.
-            if new_status.lower() == error_status.lower():
-                message = ("%s failed to get to expected status (%s). "
-                           "In %s state.") % (thing, expected_status,
-                                              new_status)
-                raise exceptions.BuildErrorException(message,
-                                                     server_id=thing_id)
-            elif new_status == expected_status and expected_status is not None:
-                return True  # All good.
-            LOG.debug("Waiting for %s to get to %s status. "
-                      "Currently in %s status",
-                      thing, log_status, new_status)
-        if not call_until_true(
-                check_status,
-                self.conf.build_timeout,
-                self.conf.build_interval):
-            message = ("Timed out waiting for thing %s "
-                       "to become %s") % (thing_id, log_status)
-            raise exceptions.TimeoutException(message)
-
     def get_remote_client(self, server_or_ip, username, private_key=None):
         if isinstance(server_or_ip, six.string_types):
             ip = server_or_ip
@@ -225,7 +161,7 @@
             return (proc.returncode == 0) == should_succeed
 
         return call_until_true(
-            ping, self.conf.build_timeout, 1)
+            self.conf.build_timeout, 1, ping)
 
     def _wait_for_resource_status(self, stack_identifier, resource_name,
                                   status, failure_pattern='^.*_FAILED$',
diff --git a/functional/test_autoscaling.py b/functional/test_autoscaling.py
index b2218a8..a8a8fc4 100644
--- a/functional/test_autoscaling.py
+++ b/functional/test_autoscaling.py
@@ -272,6 +272,8 @@
         nested_ident = self.assert_resource_is_a_stack(stack_identifier,
                                                        'JobServerGroup')
         self._assert_instance_state(nested_ident, 2, 0)
+        initial_list = [res.resource_name
+                        for res in self.client.resources.list(nested_ident)]
 
         env['parameters']['size'] = 3
         files2 = {'provider.yaml': self.bad_instance_template}
@@ -288,7 +290,46 @@
         # assert that there are 3 bad instances
         nested_ident = self.assert_resource_is_a_stack(stack_identifier,
                                                        'JobServerGroup')
-        self._assert_instance_state(nested_ident, 0, 3)
+
+        # 2 resources should be in update failed, and one create failed.
+        for res in self.client.resources.list(nested_ident):
+            if res.resource_name in initial_list:
+                self._wait_for_resource_status(nested_ident,
+                                               res.resource_name,
+                                               'UPDATE_FAILED')
+            else:
+                self._wait_for_resource_status(nested_ident,
+                                               res.resource_name,
+                                               'CREATE_FAILED')
+
+    def test_group_suspend_resume(self):
+
+        files = {'provider.yaml': self.instance_template}
+        env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
+               'parameters': {'size': 4,
+                              'image': self.conf.image_ref,
+                              'flavor': self.conf.instance_type}}
+        stack_identifier = self.stack_create(template=self.template,
+                                             files=files, environment=env)
+
+        nested_ident = self.assert_resource_is_a_stack(stack_identifier,
+                                                       'JobServerGroup')
+
+        self.client.actions.suspend(stack_id=stack_identifier)
+        self._wait_for_resource_status(
+            stack_identifier, 'JobServerGroup', 'SUSPEND_COMPLETE')
+        for res in self.client.resources.list(nested_ident):
+            self._wait_for_resource_status(nested_ident,
+                                           res.resource_name,
+                                           'SUSPEND_COMPLETE')
+
+        self.client.actions.resume(stack_id=stack_identifier)
+        self._wait_for_resource_status(
+            stack_identifier, 'JobServerGroup', 'RESUME_COMPLETE')
+        for res in self.client.resources.list(nested_ident):
+            self._wait_for_resource_status(nested_ident,
+                                           res.resource_name,
+                                           'RESUME_COMPLETE')
 
 
 class AutoscalingGroupUpdatePolicyTest(AutoscalingGroupTest):
diff --git a/functional/test_aws_stack.py b/functional/test_aws_stack.py
new file mode 100644
index 0000000..2e2cd9d
--- /dev/null
+++ b/functional/test_aws_stack.py
@@ -0,0 +1,213 @@
+#    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 hashlib
+import json
+import logging
+import random
+import urlparse
+
+from swiftclient import utils as swiftclient_utils
+import yaml
+
+from heat_integrationtests.common import test
+
+
+LOG = logging.getLogger(__name__)
+
+
+class AwsStackTest(test.HeatIntegrationTest):
+    test_template = '''
+HeatTemplateFormatVersion: '2012-12-12'
+Resources:
+  the_nested:
+    Type: AWS::CloudFormation::Stack
+    Properties:
+      TemplateURL: the.yaml
+      Parameters:
+        KeyName: foo
+Outputs:
+  output_foo:
+    Value: {"Fn::GetAtt": [the_nested, Outputs.Foo]}
+'''
+
+    nested_template = '''
+HeatTemplateFormatVersion: '2012-12-12'
+Parameters:
+  KeyName:
+    Type: String
+Outputs:
+  Foo:
+    Value: bar
+'''
+
+    update_template = '''
+HeatTemplateFormatVersion: '2012-12-12'
+Parameters:
+  KeyName:
+    Type: String
+Outputs:
+  Foo:
+    Value: foo
+'''
+
+    nested_with_res_template = '''
+HeatTemplateFormatVersion: '2012-12-12'
+Parameters:
+  KeyName:
+    Type: String
+Resources:
+  NestedResource:
+    Type: OS::Heat::RandomString
+Outputs:
+  Foo:
+    Value: {"Fn::GetAtt": [NestedResource, value]}
+'''
+
+    def setUp(self):
+        super(AwsStackTest, self).setUp()
+        self.client = self.orchestration_client
+        self.object_container_name = AwsStackTest.__name__
+        self.project_id = self.identity_client.auth_ref.project_id
+        self.object_client.put_container(self.object_container_name)
+        self.nested_name = '%s.yaml' % test.rand_name()
+
+    def publish_template(self, name, contents):
+        oc = self.object_client
+
+        # post the object
+        oc.put_object(self.object_container_name, name, contents)
+        # TODO(asalkeld) see if this is causing problems.
+        # self.addCleanup(self.object_client.delete_object,
+        #                self.object_container_name, name)
+
+        # make the tempurl
+        key_header = 'x-account-meta-temp-url-key'
+        if key_header not in oc.head_account():
+            swift_key = hashlib.sha224(
+                str(random.getrandbits(256))).hexdigest()[:32]
+            LOG.warn('setting swift key to %s' % swift_key)
+            oc.post_account({key_header: swift_key})
+        key = oc.head_account()[key_header]
+        path = '/v1/AUTH_%s/%s/%s' % (self.project_id,
+                                      self.object_container_name, name)
+        timeout = self.conf.build_timeout * 10
+        tempurl = swiftclient_utils.generate_temp_url(path, timeout,
+                                                      key, 'GET')
+        sw_url = urlparse.urlparse(oc.url)
+        return '%s://%s%s' % (sw_url.scheme, sw_url.netloc, tempurl)
+
+    def test_nested_stack_create(self):
+        url = self.publish_template(self.nested_name, self.nested_template)
+        self.template = self.test_template.replace('the.yaml', url)
+        stack_identifier = self.stack_create(template=self.template)
+        stack = self.client.stacks.get(stack_identifier)
+        self.assert_resource_is_a_stack(stack_identifier, 'the_nested')
+        self.assertEqual('bar', self._stack_output(stack, 'output_foo'))
+
+    def test_nested_stack_create_with_timeout(self):
+        url = self.publish_template(self.nested_name, self.nested_template)
+        self.template = self.test_template.replace('the.yaml', url)
+        timeout_template = yaml.load(self.template)
+        props = timeout_template['Resources']['the_nested']['Properties']
+        props['TimeoutInMinutes'] = '50'
+
+        stack_identifier = self.stack_create(
+            template=timeout_template)
+        stack = self.client.stacks.get(stack_identifier)
+        self.assert_resource_is_a_stack(stack_identifier, 'the_nested')
+        self.assertEqual('bar', self._stack_output(stack, 'output_foo'))
+
+    def test_nested_stack_adopt_ok(self):
+        url = self.publish_template(self.nested_name,
+                                    self.nested_with_res_template)
+        self.template = self.test_template.replace('the.yaml', url)
+        adopt_data = {
+            "resources": {
+                "the_nested": {
+                    "resource_id": "test-res-id",
+                    "resources": {
+                        "NestedResource": {
+                            "type": "OS::Heat::RandomString",
+                            "resource_id": "test-nested-res-id",
+                            "resource_data": {"value": "goopie"}
+                        }
+                    }
+                }
+            },
+            "environment": {"parameters": {}},
+            "template": yaml.load(self.template)
+        }
+
+        stack_identifier = self.stack_adopt(adopt_data=json.dumps(adopt_data))
+
+        self.assert_resource_is_a_stack(stack_identifier, 'the_nested')
+        stack = self.client.stacks.get(stack_identifier)
+        self.assertEqual('goopie', self._stack_output(stack, 'output_foo'))
+
+    def test_nested_stack_adopt_fail(self):
+        url = self.publish_template(self.nested_name,
+                                    self.nested_with_res_template)
+        self.template = self.test_template.replace('the.yaml', url)
+        adopt_data = {
+            "resources": {
+                "the_nested": {
+                    "resource_id": "test-res-id",
+                    "resources": {
+                    }
+                }
+            },
+            "environment": {"parameters": {}},
+            "template": yaml.load(self.template)
+        }
+
+        stack_identifier = self.stack_adopt(adopt_data=json.dumps(adopt_data),
+                                            wait_for_status='ADOPT_FAILED')
+        rsrc = self.client.resources.get(stack_identifier, 'the_nested')
+        self.assertEqual('ADOPT_FAILED', rsrc.resource_status)
+
+    def test_nested_stack_update(self):
+        url = self.publish_template(self.nested_name, self.nested_template)
+        self.template = self.test_template.replace('the.yaml', url)
+        stack_identifier = self.stack_create(template=self.template)
+        original_nested_id = self.assert_resource_is_a_stack(
+            stack_identifier, 'the_nested')
+        stack = self.client.stacks.get(stack_identifier)
+        self.assertEqual('bar', self._stack_output(stack, 'output_foo'))
+
+        new_template = yaml.load(self.template)
+        props = new_template['Resources']['the_nested']['Properties']
+        props['TemplateURL'] = self.publish_template(self.nested_name,
+                                                     self.update_template)
+
+        self.update_stack(stack_identifier, new_template)
+
+        # Expect the physical resource name staying the same after update,
+        # so that the nested was actually updated instead of replaced.
+        new_nested_id = self.assert_resource_is_a_stack(
+            stack_identifier, 'the_nested')
+        self.assertEqual(original_nested_id, new_nested_id)
+        updt_stack = self.client.stacks.get(stack_identifier)
+        self.assertEqual('foo', self._stack_output(updt_stack, 'output_foo'))
+
+    def test_nested_stack_suspend_resume(self):
+        url = self.publish_template(self.nested_name, self.nested_template)
+        self.template = self.test_template.replace('the.yaml', url)
+        stack_identifier = self.stack_create(template=self.template)
+
+        self.client.actions.suspend(stack_id=stack_identifier)
+        self._wait_for_resource_status(
+            stack_identifier, 'the_nested', 'SUSPEND_COMPLETE')
+
+        self.client.actions.resume(stack_id=stack_identifier)
+        self._wait_for_resource_status(
+            stack_identifier, 'the_nested', 'RESUME_COMPLETE')
diff --git a/functional/test_heat_autoscaling.py b/functional/test_heat_autoscaling.py
new file mode 100644
index 0000000..340038c
--- /dev/null
+++ b/functional/test_heat_autoscaling.py
@@ -0,0 +1,99 @@
+#    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.common import test
+
+
+class HeatAutoscalingTest(test.HeatIntegrationTest):
+    template = '''
+heat_template_version: 2014-10-16
+
+resources:
+  random_group:
+    type: OS::Heat::AutoScalingGroup
+    properties:
+      max_size: 10
+      min_size: 10
+      resource:
+        type: OS::Heat::RandomString
+
+outputs:
+  all_values:
+    value: {get_attr: [random_group, outputs_list, value]}
+  value_0:
+    value: {get_attr: [random_group, resource.0.value]}
+  value_5:
+    value: {get_attr: [random_group, resource.5.value]}
+  value_9:
+    value: {get_attr: [random_group, resource.9.value]}
+'''
+
+    template_nested = '''
+heat_template_version: 2014-10-16
+
+resources:
+  random_group:
+    type: OS::Heat::AutoScalingGroup
+    properties:
+      max_size: 10
+      min_size: 10
+      resource:
+        type: randomstr.yaml
+
+outputs:
+  all_values:
+    value: {get_attr: [random_group, outputs_list, random_str]}
+  value_0:
+    value: {get_attr: [random_group, resource.0.random_str]}
+  value_5:
+    value: {get_attr: [random_group, resource.5.random_str]}
+  value_9:
+    value: {get_attr: [random_group, resource.9.random_str]}
+'''
+
+    template_randomstr = '''
+heat_template_version: 2013-05-23
+
+resources:
+  random_str:
+    type: OS::Heat::RandomString
+
+outputs:
+  random_str:
+    value: {get_attr: [random_str, value]}
+'''
+
+    def setUp(self):
+        super(HeatAutoscalingTest, self).setUp()
+        self.client = self.orchestration_client
+
+    def _assert_output_values(self, stack_id):
+        stack = self.client.stacks.get(stack_id)
+        all_values = self._stack_output(stack, 'all_values')
+        self.assertEqual(10, len(all_values))
+        self.assertEqual(all_values[0], self._stack_output(stack, 'value_0'))
+        self.assertEqual(all_values[5], self._stack_output(stack, 'value_5'))
+        self.assertEqual(all_values[9], self._stack_output(stack, 'value_9'))
+
+    def test_path_attrs(self):
+        stack_id = self.stack_create(template=self.template)
+        expected_resources = {'random_group': 'OS::Heat::AutoScalingGroup'}
+        self.assertEqual(expected_resources, self.list_resources(stack_id))
+        self._assert_output_values(stack_id)
+
+    def test_path_attrs_nested(self):
+        files = {'randomstr.yaml': self.template_randomstr}
+        stack_id = self.stack_create(template=self.template_nested,
+                                     files=files)
+        expected_resources = {'random_group': 'OS::Heat::AutoScalingGroup'}
+        self.assertEqual(expected_resources, self.list_resources(stack_id))
+        self._assert_output_values(stack_id)
diff --git a/functional/test_instance_group.py b/functional/test_instance_group.py
index c76e67a..84c63cd 100644
--- a/functional/test_instance_group.py
+++ b/functional/test_instance_group.py
@@ -270,6 +270,8 @@
         nested_ident = self.assert_resource_is_a_stack(stack_identifier,
                                                        'JobServerGroup')
         self._assert_instance_state(nested_ident, 2, 0)
+        initial_list = [res.resource_name
+                        for res in self.client.resources.list(nested_ident)]
 
         env['parameters']['size'] = 3
         files2 = {'provider.yaml': self.bad_instance_template}
@@ -283,10 +285,19 @@
         )
         self._wait_for_stack_status(stack_identifier, 'UPDATE_FAILED')
 
-        # assert that there are 3 bad instances
         nested_ident = self.assert_resource_is_a_stack(stack_identifier,
                                                        'JobServerGroup')
-        self._assert_instance_state(nested_ident, 0, 3)
+        # assert that there are 3 bad instances
+        # 2 resources should be in update failed, and one create failed.
+        for res in self.client.resources.list(nested_ident):
+            if res.resource_name in initial_list:
+                self._wait_for_resource_status(nested_ident,
+                                               res.resource_name,
+                                               'UPDATE_FAILED')
+            else:
+                self._wait_for_resource_status(nested_ident,
+                                               res.resource_name,
+                                               'CREATE_FAILED')
 
 
 class InstanceGroupUpdatePolicyTest(InstanceGroupTest):
diff --git a/functional/test_template_resource.py b/functional/test_template_resource.py
index 39855be..5df4235 100644
--- a/functional/test_template_resource.py
+++ b/functional/test_template_resource.py
@@ -337,6 +337,54 @@
                                 self._stack_output(stack, 'value'))
 
 
+class TemplateResourceUpdateFailedTest(test.HeatIntegrationTest):
+    """Prove that we can do updates on a nested stack to fix a stack."""
+    main_template = '''
+HeatTemplateFormatVersion: '2012-12-12'
+Resources:
+  keypair:
+    Type: OS::Nova::KeyPair
+    Properties:
+      name: replace-this
+      save_private_key: false
+  server:
+    Type: server_fail.yaml
+    DependsOn: keypair
+'''
+    nested_templ = '''
+HeatTemplateFormatVersion: '2012-12-12'
+Resources:
+  RealRandom:
+    Type: OS::Heat::RandomString
+'''
+
+    def setUp(self):
+        super(TemplateResourceUpdateFailedTest, self).setUp()
+        self.client = self.orchestration_client
+        if self.conf.keypair_name:
+            self.keypair_name = self.conf.keypair_name
+        else:
+            self.keypair = self.create_keypair()
+            self.keypair_name = self.keypair.id
+
+    def test_update_on_failed_create(self):
+        # create a stack with "server" dependent on "keypair", but
+        # keypair fails, so "server" is not created properly.
+        # We then fix the template and it should succeed.
+        broken_templ = self.main_template.replace('replace-this',
+                                                  self.keypair_name)
+        stack_identifier = self.stack_create(
+            template=broken_templ,
+            files={'server_fail.yaml': self.nested_templ},
+            expected_status='CREATE_FAILED')
+
+        fixed_templ = self.main_template.replace('replace-this',
+                                                 test.rand_name())
+        self.update_stack(stack_identifier,
+                          fixed_templ,
+                          files={'server_fail.yaml': self.nested_templ})
+
+
 class TemplateResourceAdoptTest(test.HeatIntegrationTest):
     """Prove that we can do template resource adopt/abandon."""
 
diff --git a/scenario/test_neutron_autoscaling.py b/scenario/test_neutron_autoscaling.py
index 8f769ef..be01328 100644
--- a/scenario/test_neutron_autoscaling.py
+++ b/scenario/test_neutron_autoscaling.py
@@ -72,7 +72,7 @@
 
 
 class NeutronAutoscalingTest(test.HeatIntegrationTest):
-    """"
+    """
     The class is responsible for testing of neutron resources autoscaling.
     """
 
@@ -103,8 +103,7 @@
         env = {'parameters': {"image_id": self.conf.minimal_image_ref,
                               "capacity": "1",
                               "instance_type": self.conf.instance_type,
-                              "fixed_subnet_name":
-                                  self.conf.fixed_subnet_name,
+                              "fixed_subnet_name": self.conf.fixed_subnet_name,
                               }}
 
         # Create stack
@@ -121,4 +120,4 @@
                           environment=env)
 
         upd_members = self.network_client.list_members()
-        self.assertEqual(2, len(upd_members["members"]))
\ No newline at end of file
+        self.assertEqual(2, len(upd_members["members"]))