Increase the concurrency of software-config functional test

The test now creates up to 10 deployments in various stack/deployment
resource combinations and asserts that the resulting server metadata
has the appropriate number of resources.

This test now creates a deployment via OS::Heat::SoftwareDeployments as
the extra nested stack is required to trigger the race causing bug #1477329

Related-Bug: #1477329
Change-Id: Id3d5b09def6d03bca669c1ab0751641b72f61ce4
diff --git a/functional/test_software_config.py b/functional/test_software_config.py
index 2f38c55..019e6c9 100644
--- a/functional/test_software_config.py
+++ b/functional/test_software_config.py
@@ -10,11 +10,17 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from oslo_utils import timeutils
+import requests
+import time
+import yaml
+
+from heat_integrationtests.common import exceptions
 from heat_integrationtests.functional import functional_base
 
 
 class ParallelDeploymentsTest(functional_base.FunctionalTestsBase):
-    template = '''
+    server_template = '''
 heat_template_version: "2013-05-23"
 parameters:
   flavor:
@@ -30,47 +36,124 @@
       image: {get_param: image}
       flavor: {get_param: flavor}
       user_data_format: SOFTWARE_CONFIG
-      networks: [{network: {get_param: network} }]
+      networks: [{network: {get_param: network}}]
+outputs:
+  server:
+    value: {get_resource: server}
+'''
+
+    config_template = '''
+heat_template_version: "2013-05-23"
+parameters:
+  server:
+    type: string
+resources:
   config:
     type: OS::Heat::SoftwareConfig
     properties:
-      config: hi!
-  dep1:
-    type: OS::Heat::SoftwareDeployment
-    properties:
-      config: {get_resource: config}
-      server: {get_resource: server}
-      signal_transport: NO_SIGNAL
-  dep2:
-    type: OS::Heat::SoftwareDeployment
-    properties:
-      config: {get_resource: config}
-      server: {get_resource: server}
-      signal_transport: NO_SIGNAL
-  dep3:
-    type: OS::Heat::SoftwareDeployment
-    properties:
-      config: {get_resource: config}
-      server: {get_resource: server}
-      signal_transport: NO_SIGNAL
-  dep4:
-    type: OS::Heat::SoftwareDeployment
-    properties:
-      config: {get_resource: config}
-      server: {get_resource: server}
-      signal_transport: NO_SIGNAL
 '''
 
-    def setUp(self):
-        super(ParallelDeploymentsTest, self).setUp()
+    deployment_snippet = '''
+type: OS::Heat::SoftwareDeployments
+properties:
+  config: {get_resource: config}
+  servers: {'0': {get_param: server}}
+'''
 
-    def test_fail(self):
+    enable_cleanup = True
+
+    def test_deployments_metadata(self):
         parms = {'flavor': self.conf.minimal_instance_type,
                  'network': self.conf.fixed_network_name,
                  'image': self.conf.minimal_image_ref}
         stack_identifier = self.stack_create(
             parameters=parms,
-            template=self.template)
-        stack = self.client.stacks.get(stack_identifier)
-        server_metadata = self.client.resources.metadata(stack.id, 'server')
-        self.assertEqual(4, len(server_metadata['deployments']))
+            template=self.server_template,
+            enable_cleanup=self.enable_cleanup)
+        server_stack = self.client.stacks.get(stack_identifier)
+        server = server_stack.outputs[0]['output_value']
+
+        config_stacks = []
+        # add up to 3 stacks each with up to 3 deployments
+        deploy_count = 0
+        deploy_count = self.deploy_many_configs(
+            stack_identifier,
+            server,
+            config_stacks,
+            2,
+            5,
+            deploy_count)
+        deploy_count = self.deploy_many_configs(
+            stack_identifier,
+            server,
+            config_stacks,
+            3,
+            3,
+            deploy_count)
+
+        self.signal_deployments(stack_identifier)
+        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,
+                            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)
+        )
+        return new_count
+
+    def deploy_config(self, server, deploy_count):
+        parms = {'server': server}
+        template = yaml.safe_load(self.config_template)
+        resources = template['resources']
+        resources['config']['properties'] = {'config': 'x' * 10000}
+        for a in range(deploy_count):
+            resources['dep_%s' % a] = yaml.safe_load(self.deployment_snippet)
+        return self.stack_create(
+            parameters=parms,
+            template=template,
+            enable_cleanup=self.enable_cleanup,
+            expected_status=None)
+
+    def wait_for_deploy_physical_id(self, stack):
+        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:
+                return
+            time.sleep(build_interval)
+
+        message = ('Deployment resources failed to be created within '
+                   'the required time (%s s).' %
+                   (build_timeout))
+        raise exceptions.TimeoutException(message)
+
+    def signal_deployments(self, stack_identifier):
+        server_metadata = self.client.resources.metadata(
+            stack_identifier, 'server')
+        for dep in server_metadata['deployments']:
+            iv = dict((i['name'], i['value']) for i in dep['inputs'])
+            sigurl = iv.get('deploy_signal_id')
+            requests.post(sigurl, data='{}',
+                          headers={'content-type': None})