Add test for SoftwareDeploymentGroup resource

Adds an initial test which tests create/delete for this resource,
and that signalling works.

Also tests deleting an in-progress stack as this has been a repeated
source of bugs related to deleting the child stack correctly.

Change-Id: I0d5acdca50467da344388d6c262e61aaaaae22eb
Related-Bug: #1592374
diff --git a/functional/test_software_deployment_group.py b/functional/test_software_deployment_group.py
new file mode 100644
index 0000000..7817c2b
--- /dev/null
+++ b/functional/test_software_deployment_group.py
@@ -0,0 +1,120 @@
+#    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 SoftwareDeploymentGroupTest(functional_base.FunctionalTestsBase):
+    sd_template = '''
+heat_template_version: 2016-10-14
+
+parameters:
+  input:
+    type: string
+    default: foo_input
+
+resources:
+  config:
+    type: OS::Heat::SoftwareConfig
+    properties:
+      group: script
+      inputs:
+      - name: foo
+
+  deployment:
+    type: OS::Heat::SoftwareDeploymentGroup
+    properties:
+      config: {get_resource: config}
+      input_values:
+        foo: {get_param: input}
+      servers:
+        '0': dummy0
+        '1': dummy1
+        '2': dummy2
+        '3': dummy3
+'''
+
+    enable_cleanup = True
+
+    def test_deployments_crud(self):
+        stack_identifier = self.stack_create(
+            template=self.sd_template,
+            enable_cleanup=self.enable_cleanup,
+            expected_status='CREATE_IN_PROGRESS')
+        self._wait_for_resource_status(
+            stack_identifier, 'deployment', 'CREATE_IN_PROGRESS')
+        nested_identifier = self.assert_resource_is_a_stack(
+            stack_identifier, 'deployment')
+        group_resources = self.list_group_resources(
+            stack_identifier, 'deployment', minimal=False)
+
+        self.assertEqual(4, len(group_resources))
+        self.signal_deployments(group_resources)
+        self._wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
+
+        self.check_input_values(group_resources, 'foo_input')
+
+        self.update_stack(stack_identifier,
+                          template=self.sd_template,
+                          environment={'parameters': {'input': 'input2'}},
+                          expected_status='UPDATE_IN_PROGRESS')
+        nested_identifier = self.assert_resource_is_a_stack(
+            stack_identifier, 'deployment')
+        self.assertEqual(4, len(group_resources))
+        self.signal_deployments(group_resources)
+        self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
+
+        self.check_input_values(group_resources, 'input2')
+
+        # We explicitly test delete here, vs just via cleanup and check
+        # the nested stack is gone
+        self._stack_delete(stack_identifier)
+        self._wait_for_stack_status(
+            nested_identifier, 'DELETE_COMPLETE',
+            success_on_not_found=True)
+
+    def test_deployments_create_delete_in_progress(self):
+        stack_identifier = self.stack_create(
+            template=self.sd_template,
+            enable_cleanup=self.enable_cleanup,
+            expected_status='CREATE_IN_PROGRESS')
+        self._wait_for_resource_status(
+            stack_identifier, 'deployment', 'CREATE_IN_PROGRESS')
+        nested_identifier = self.assert_resource_is_a_stack(
+            stack_identifier, 'deployment')
+        group_resources = self.list_group_resources(
+            stack_identifier, 'deployment', minimal=False)
+
+        self.assertEqual(4, len(group_resources))
+        # Now test delete while the stacks are still IN_PROGRESS
+        self._stack_delete(stack_identifier)
+        self._wait_for_stack_status(
+            nested_identifier, 'DELETE_COMPLETE',
+            success_on_not_found=True)
+
+    def check_input_values(self, group_resources, value):
+        # Check inputs for deployment and derived config
+        for r in group_resources:
+            d = self.client.software_deployments.get(
+                r.physical_resource_id)
+            self.assertEqual({'foo': value}, d.input_values)
+            c = self.client.software_configs.get(
+                d.config_id)
+            foo_input_c = [i for i in c.inputs if i.get('name') == 'foo'][0]
+            self.assertEqual(value, foo_input_c.get('value'))
+
+    def signal_deployments(self, resources):
+        # Signal all IN_PROGRESS deployment resources
+        for r in resources:
+            if 'IN_PROGRESS' in r.resource_status:
+                stack_id = self.get_resource_stack_id(r)
+                self.client.resources.signal(stack_id, r.resource_name)