Merge "Move template resource tests to functional"
diff --git a/common/test.py b/common/test.py
index 25703f2..57083fd 100644
--- a/common/test.py
+++ b/common/test.py
@@ -347,7 +347,8 @@
         return dict((r.resource_name, r.resource_type) for r in resources)
 
     def stack_create(self, stack_name=None, template=None, files=None,
-                     parameters=None, environment=None):
+                     parameters=None, environment=None,
+                     expected_status='CREATE_COMPLETE'):
         name = stack_name or self._stack_rand_name()
         templ = template or self.template
         templ_files = files or {}
@@ -365,7 +366,7 @@
 
         stack = self.client.stacks.get(name)
         stack_identifier = '%s/%s' % (name, stack.id)
-        self._wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
+        self._wait_for_stack_status(stack_identifier, expected_status)
         return stack_identifier
 
     def stack_adopt(self, stack_name=None, files=None,
diff --git a/functional/test_instance_group.py b/functional/test_instance_group.py
index 9a79801..d4e85b9 100644
--- a/functional/test_instance_group.py
+++ b/functional/test_instance_group.py
@@ -10,11 +10,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import copy
 import logging
-import yaml
-
-from heatclient import exc
 
 from heat_integrationtests.common import test
 
@@ -31,8 +27,7 @@
   "Parameters" : {"size": {"Type": "String", "Default": "1"},
                   "AZ": {"Type": "String", "Default": "nova"},
                   "image": {"Type": "String"},
-                  "flavor": {"Type": "String"},
-                  "keyname": {"Type": "String"}},
+                  "flavor": {"Type": "String"}},
   "Resources": {
     "JobServerGroup": {
       "Type": "OS::Heat::InstanceGroup",
@@ -49,7 +44,6 @@
       "Properties": {
         "ImageId"           : {"Ref": "image"},
         "InstanceType"      : {"Ref": "flavor"},
-        "KeyName"           : {"Ref": "keyname"},
         "SecurityGroups"    : [ "sg-1" ],
         "UserData"          : "jsconfig data"
       }
@@ -67,7 +61,6 @@
 parameters:
   ImageId: {type: string}
   InstanceType: {type: string}
-  KeyName: {type: string}
   SecurityGroups: {type: comma_delimited_list}
   UserData: {type: string}
   Tags: {type: comma_delimited_list}
@@ -87,7 +80,6 @@
 parameters:
   ImageId: {type: string}
   InstanceType: {type: string}
-  KeyName: {type: string}
   SecurityGroups: {type: comma_delimited_list}
   UserData: {type: string}
   Tags: {type: comma_delimited_list}
@@ -113,8 +105,6 @@
         self.client = self.orchestration_client
         if not self.conf.image_ref:
             raise self.skipException("No image configured to test")
-        if not self.conf.keypair_name:
-            raise self.skipException("No keyname configured to test")
         if not self.conf.instance_type:
             raise self.skipException("No flavor configured to test")
 
@@ -155,7 +145,6 @@
         env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
                'parameters': {'size': 4,
                               'image': self.conf.image_ref,
-                              'keyname': self.conf.keypair_name,
                               'flavor': self.conf.instance_type}}
         stack_identifier = self.stack_create(template=self.template,
                                              files=files, environment=env)
@@ -168,37 +157,11 @@
         stack = self.client.stacks.get(stack_identifier)
         self.assert_instance_count(stack, 4)
 
-    def test_create_config_prop_validation(self):
-        """Make sure that during a group create the instance
-        properties are validated. And an error causes the group to fail.
-        """
-        stack_name = self._stack_rand_name()
-
-        # add a property without a default and don't provide a value.
-        # we use this to make the instance fail on a property validation
-        # error.
-        broken = yaml.load(copy.copy(self.instance_template))
-        broken['parameters']['no_default'] = {'type': 'string'}
-        files = {'provider.yaml': yaml.dump(broken)}
-        env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
-               'parameters': {'size': 4,
-                              'image': self.conf.image_ref,
-                              'keyname': self.conf.keypair_name,
-                              'flavor': self.conf.instance_type}}
-
-        # now with static nested stack validation, this gets raised quickly.
-        excp = self.assertRaises(exc.HTTPBadRequest, self.client.stacks.create,
-                                 stack_name=stack_name, template=self.template,
-                                 files=files, disable_rollback=True,
-                                 parameters={}, environment=env)
-        self.assertIn('Property no_default not assigned', str(excp))
-
     def test_size_updates_work(self):
         files = {'provider.yaml': self.instance_template}
         env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
                'parameters': {'size': 2,
                               'image': self.conf.image_ref,
-                              'keyname': self.conf.keypair_name,
                               'flavor': self.conf.instance_type}}
 
         stack_identifier = self.stack_create(template=self.template,
@@ -211,7 +174,6 @@
         env2 = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
                 'parameters': {'size': 5,
                                'image': self.conf.image_ref,
-                               'keyname': self.conf.keypair_name,
                                'flavor': self.conf.instance_type}}
         self.update_stack(stack_identifier, self.template,
                           environment=env2, files=files)
@@ -228,7 +190,6 @@
                {'AWS::EC2::Instance': 'provider.yaml'},
                'parameters': {'size': 1,
                               'image': self.conf.image_ref,
-                              'keyname': self.conf.keypair_name,
                               'flavor': self.conf.instance_type}}
 
         stack_identifier = self.stack_create(template=self.template,
@@ -242,7 +203,6 @@
                 'parameters': {'size': '2',
                                'AZ': 'wibble',
                                'image': self.conf.image_ref,
-                               'keyname': self.conf.keypair_name,
                                'flavor': self.conf.instance_type}}
         self.update_stack(stack_identifier, self.template,
                           environment=env2, files=files)
@@ -260,7 +220,6 @@
         env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
                'parameters': {'size': 2,
                               'image': self.conf.image_ref,
-                              'keyname': self.conf.keypair_name,
                               'flavor': self.conf.instance_type}}
 
         self.client.stacks.create(
@@ -293,7 +252,6 @@
         env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
                'parameters': {'size': 2,
                               'image': self.conf.image_ref,
-                              'keyname': self.conf.keypair_name,
                               'flavor': self.conf.instance_type}}
 
         stack_identifier = self.stack_create(template=self.template,
diff --git a/functional/test_resource_group.py b/functional/test_resource_group.py
index afd9f55..8ccddd8 100644
--- a/functional/test_resource_group.py
+++ b/functional/test_resource_group.py
@@ -17,6 +17,27 @@
 from heat_integrationtests.common import test
 
 
+template = '''
+heat_template_version: 2013-05-23
+resources:
+  random_group:
+    type: OS::Heat::ResourceGroup
+    properties:
+      count: 2
+      resource_def:
+        type: OS::Heat::RandomString
+        properties:
+          length: 30
+outputs:
+  random1:
+    value: {get_attr: [random_group, resource.0.value]}
+  random2:
+    value: {get_attr: [random_group, resource.1.value]}
+  all_values:
+    value: {get_attr: [random_group, value]}
+'''
+
+
 class ResourceGroupTest(test.HeatIntegrationTest):
 
     def setUp(self):
@@ -90,3 +111,54 @@
                                template=template_two_nested,
                                environment=env, files=files)
         self.assertIn(expected_err, six.text_type(ex))
+
+    def _validate_resources(self, stack_identifier, expected_count):
+        nested_identifier = self._group_nested_identifier(stack_identifier)
+        resources = self.list_resources(nested_identifier)
+        self.assertEqual(expected_count, len(resources))
+        expected_resources = dict(
+            (str(idx), 'OS::Heat::RandomString')
+            for idx in range(expected_count))
+
+        self.assertEqual(expected_resources, resources)
+
+    def test_create(self):
+        def validate_output(stack, output_key, length):
+            output_value = self._stack_output(stack, output_key)
+            self.assertEqual(length, len(output_value))
+            return output_value
+        # verify that the resources in resource group are identically
+        # configured, resource names and outputs are appropriate.
+        stack_identifier = self.stack_create('create_stack', template=template)
+        self.assertEqual({u'random_group': u'OS::Heat::ResourceGroup'},
+                         self.list_resources(stack_identifier))
+
+        # validate count, type and name of resources in a resource group.
+        self._validate_resources(stack_identifier, 2)
+
+        # validate outputs
+        stack = self.client.stacks.get(stack_identifier)
+        outputs = []
+        outputs.append(validate_output(stack, 'random1', 30))
+        outputs.append(validate_output(stack, 'random2', 30))
+        self.assertEqual(outputs, self._stack_output(stack, 'all_values'))
+
+    def test_update_increase_decrease_count(self):
+        # create stack with resource group count 2
+        stack_identifier = self.stack_create('update_stack', template=template)
+        self.assertEqual({u'random_group': u'OS::Heat::ResourceGroup'},
+                         self.list_resources(stack_identifier))
+        # verify that the resource group has 2 resources
+        self._validate_resources(stack_identifier, 2)
+
+        # increase the resource group count to 5
+        update_template = template.replace("count: 2", "count: 5")
+        self.update_stack(stack_identifier, update_template)
+        # verify that the resource group has 5 resources
+        self._validate_resources(stack_identifier, 5)
+
+        # decrease the resource group count to 3
+        update_template = template.replace("count: 2", "count: 3")
+        self.update_stack(stack_identifier, update_template)
+        # verify that the resource group has 3 resources
+        self._validate_resources(stack_identifier, 3)
diff --git a/functional/test_validation.py b/functional/test_validation.py
new file mode 100644
index 0000000..52309cd
--- /dev/null
+++ b/functional/test_validation.py
@@ -0,0 +1,95 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import logging
+
+from heat_integrationtests.common import test
+
+
+LOG = logging.getLogger(__name__)
+
+
+class StackValidationTest(test.HeatIntegrationTest):
+
+    def setUp(self):
+        super(StackValidationTest, self).setUp()
+        self.client = self.orchestration_client
+        if not self.conf.minimal_image_ref:
+            raise self.skipException("No image configured to test")
+
+        if not self.conf.instance_type:
+            raise self.skipException("No instance_type configured to test")
+
+        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_stack_validate_provider_references_parent_resource(self):
+        template = '''
+heat_template_version: 2014-10-16
+parameters:
+  keyname:
+    type: string
+  flavor:
+    type: string
+  image:
+    type: string
+resources:
+  config:
+    type: My::Config
+    properties:
+        server: {get_resource: server}
+
+  server:
+    type: OS::Nova::Server
+    properties:
+      image: {get_param: image}
+      flavor: {get_param: flavor}
+      key_name: {get_param: keyname}
+      user_data_format: SOFTWARE_CONFIG
+'''
+        config_template = '''
+heat_template_version: 2014-10-16
+parameters:
+  server:
+    type: string
+resources:
+  config:
+    type: OS::Heat::SoftwareConfig
+
+  deployment:
+    type: OS::Heat::SoftwareDeployment
+    properties:
+      config:
+        get_resource: config
+      server:
+        get_param: server
+'''
+        files = {'provider.yaml': config_template}
+        env = {'resource_registry':
+               {'My::Config': 'provider.yaml'}}
+        parameters = {'keyname': self.keypair_name,
+                      'flavor': self.conf.instance_type,
+                      'image': self.conf.minimal_image_ref}
+        # Note we don't wait for CREATE_COMPLETE, because we're using a
+        # minimal image without the tools to apply the config.
+        # The point of the test is just to prove that validation won't
+        # falsely prevent stack creation starting, ref bug #1407100
+        # Note that we can be sure My::Config will stay IN_PROGRESS as
+        # there's no signal sent to the deployment
+        self.stack_create(template=template,
+                          files=files,
+                          environment=env,
+                          parameters=parameters,
+                          expected_status='CREATE_IN_PROGRESS')