Merge "Revert "Temporarily skip failing scenario tests""
diff --git a/common/config.py b/common/config.py
index 0de6480..5a33590 100644
--- a/common/config.py
+++ b/common/config.py
@@ -27,7 +27,8 @@
                help="API key to use when authenticating.",
                secret=True),
     cfg.StrOpt('tenant_name',
-               default=os.environ.get('OS_TENANT_NAME'),
+               default=(os.environ.get('OS_PROJECT_NAME') or
+                        os.environ.get('OS_TENANT_NAME')),
                help="Tenant name to use for API requests."),
     cfg.StrOpt('auth_url',
                default=os.environ.get('OS_AUTH_URL'),
@@ -105,6 +106,9 @@
     cfg.BoolOpt('skip_stack_abandon_tests',
                 default=False,
                 help="Skip Stack Abandon Integration tests"),
+    cfg.BoolOpt('skip_notification_tests',
+                default=False,
+                help="Skip Notification Integration tests"),
     cfg.IntOpt('connectivity_timeout',
                default=120,
                help="Timeout in seconds to wait for connectivity to "
diff --git a/functional/test_conditional_exposure.py b/functional/test_conditional_exposure.py
new file mode 100644
index 0000000..c74b297
--- /dev/null
+++ b/functional/test_conditional_exposure.py
@@ -0,0 +1,82 @@
+#    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 heatclient import exc
+import keystoneclient
+
+from heat_integrationtests.common import test
+
+
+class ConditionalExposureTestBase(test.HeatIntegrationTest):
+    def setUp(self):
+        super(ConditionalExposureTestBase, self).setUp()
+        self.client = self.orchestration_client
+
+    def _delete(self, stack_name):
+        stacks = self.client.stacks.list()
+        for s in stacks:
+            if s.stack_name == stack_name:
+                self._stack_delete(s.identifier)
+                break
+
+
+class ServiceBasedExposureTest(ConditionalExposureTestBase):
+    # NOTE(pas-ha) if we ever decide to install Sahara on Heat
+    # functional gate, this must be changed to other not-installed
+    # but in principle supported service
+    unavailable_service = 'Sahara'
+    unavailable_template = """
+heat_template_version: 2015-10-15
+resources:
+  not_available:
+    type: OS::Sahara::NodeGroupTemplate
+    properties:
+      plugin_name: fake
+      hadoop_version: 0.1
+      flavor: m1.large
+      node_processes: []
+"""
+
+    def setUp(self):
+        super(ServiceBasedExposureTest, self).setUp()
+        # check that Sahara endpoint is available
+        if self._is_sahara_deployed():
+            self.skipTest("Sahara is actually deployed, "
+                          "can not run negative tests on "
+                          "Sahara resources availability.")
+
+    def _is_sahara_deployed(self):
+        keystone = self.identity_client
+        try:
+            keystone.service_catalog.url_for(
+                attr='region',
+                filter_value=self.conf.region,
+                service_type='data-processing',
+                endpoint_type='publicURL')
+        except keystoneclient.exceptions.EndpointNotFound:
+            return False
+        return True
+
+    def test_unavailable_resources_not_listed(self):
+        resources = self.client.resource_types.list()
+        self.assertFalse(any(self.unavailable_service in r.resource_type
+                             for r in resources))
+
+    def test_unavailable_resources_not_created(self):
+        stack_name = self._stack_rand_name()
+        self.addCleanup(self._delete, stack_name)
+        ex = self.assertRaises(exc.HTTPBadRequest,
+                               self.client.stacks.create,
+                               stack_name=stack_name,
+                               template=self.unavailable_template)
+        self.assertIn('ResourceTypeUnavailable', ex.message)
+        self.assertIn('OS::Sahara::NodeGroupTemplate', ex.message)
diff --git a/functional/test_notifications.py b/functional/test_notifications.py
index a4c419c..c729a67 100644
--- a/functional/test_notifications.py
+++ b/functional/test_notifications.py
@@ -124,6 +124,8 @@
 
     def setUp(self):
         super(NotificationTest, self).setUp()
+        if self.conf.skip_notification_tests:
+            self.skipTest('Testing Notifications disabled in conf, skipping')
 
         self.client = self.orchestration_client
         self.exchange = kombu.Exchange('heat', 'topic', durable=False)
diff --git a/functional/test_preview.py b/functional/test_preview.py
new file mode 100644
index 0000000..b4389ff
--- /dev/null
+++ b/functional/test_preview.py
@@ -0,0 +1,187 @@
+#    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
+from heatclient import exc
+import six
+
+
+class StackPreviewTest(test.HeatIntegrationTest):
+    template = '''
+heat_template_version: 2015-04-30
+parameters:
+  incomming:
+    type: string
+resources:
+  one:
+    type: OS::Heat::TestResource
+    properties:
+      value: fred
+  two:
+    type: OS::Heat::TestResource
+    properties:
+      value: {get_param: incomming}
+    depends_on: one
+outputs:
+  main_out:
+    value: {get_attr: [two, output]}
+    '''
+    env = '''
+parameters:
+  incomming: abc
+    '''
+
+    def setUp(self):
+        super(StackPreviewTest, self).setUp()
+        self.client = self.orchestration_client
+        self.project_id = self.identity_client.auth_ref.project_id
+
+    def _assert_resource(self, res, stack_name):
+        self.assertEqual(stack_name, res['stack_name'])
+        self.assertEqual('INIT', res['resource_action'])
+        self.assertEqual('COMPLETE', res['resource_status'])
+        for field in ('resource_status_reason', 'physical_resource_id',
+                      'description'):
+            self.assertIn(field, res)
+            self.assertEqual('', res[field])
+        for field in ('creation_time', 'updated_time'):
+            self.assertIn(field, res)
+            self.assertIsNotNone(res[field])
+        self.assertIn('output', res['attributes'])
+
+        # resource_identity
+        self.assertEqual(stack_name,
+                         res['resource_identity']['stack_name'])
+        self.assertEqual('None', res['resource_identity']['stack_id'])
+        self.assertEqual(self.project_id,
+                         res['resource_identity']['tenant'])
+        self.assertEqual('/resources/%s' % res['resource_name'],
+                         res['resource_identity']['path'])
+        # stack_identity
+        self.assertEqual(stack_name,
+                         res['stack_identity']['stack_name'])
+        self.assertEqual('None', res['stack_identity']['stack_id'])
+        self.assertEqual(self.project_id,
+                         res['stack_identity']['tenant'])
+        self.assertEqual('', res['stack_identity']['path'])
+
+    def _assert_results(self, result, stack_name):
+        # global stuff.
+        self.assertEqual(stack_name, result['stack_name'])
+        self.assertTrue(result['disable_rollback'])
+        self.assertEqual('None', result['id'])
+        self.assertIsNone(result['parent'])
+        self.assertEqual('No description', result['template_description'])
+
+        # parameters
+        self.assertEqual('None', result['parameters']['OS::stack_id'])
+        self.assertEqual(stack_name, result['parameters']['OS::stack_name'])
+        self.assertEqual('abc', result['parameters']['incomming'])
+
+    def test_basic_pass(self):
+        stack_name = self._stack_rand_name()
+        result = self.client.stacks.preview(
+            template=self.template,
+            stack_name=stack_name,
+            disable_rollback=True,
+            environment=self.env).to_dict()
+
+        self._assert_results(result, stack_name)
+        for res in result['resources']:
+            self._assert_resource(res, stack_name)
+            self.assertEqual('OS::Heat::TestResource',
+                             res['resource_type'])
+
+            # common properties
+            self.assertEqual(False, res['properties']['fail'])
+            self.assertEqual(0, res['properties']['wait_secs'])
+            self.assertEqual(False, res['properties']['update_replace'])
+
+            if res['resource_name'] == 'one':
+                self.assertEqual('fred', res['properties']['value'])
+                self.assertEqual(['two'], res['required_by'])
+            if res['resource_name'] == 'two':
+                self.assertEqual('abc', res['properties']['value'])
+                self.assertEqual([], res['required_by'])
+
+    def test_basic_fail(self):
+        stack_name = self._stack_rand_name()
+
+        # break the template so it fails validation.
+        wont_work = self.template.replace('get_param: incomming',
+                                          'get_param: missing')
+        excp = self.assertRaises(exc.HTTPBadRequest,
+                                 self.client.stacks.preview,
+                                 template=wont_work,
+                                 stack_name=stack_name,
+                                 disable_rollback=True,
+                                 environment=self.env)
+
+        self.assertIn('Property error: : resources.two.properties.value: '
+                      ': The Parameter (missing) was not provided.',
+                      six.text_type(excp))
+
+    def test_nested_pass(self):
+        """Nested stacks need to recurse down the stacks."""
+        main_template = '''
+heat_template_version: 2015-04-30
+parameters:
+  incomming:
+    type: string
+resources:
+  main:
+    type: nested.yaml
+    properties:
+      value: {get_param: incomming}
+outputs:
+  main_out:
+    value: {get_attr: [main, output]}
+    '''
+        nested_template = '''
+heat_template_version: 2015-04-30
+parameters:
+  value:
+    type: string
+resources:
+  nested:
+    type: OS::Heat::TestResource
+    properties:
+      value: {get_param: value}
+outputs:
+  output:
+    value: {get_attr: [nested, output]}
+'''
+        stack_name = self._stack_rand_name()
+        result = self.client.stacks.preview(
+            disable_rollback=True,
+            stack_name=stack_name,
+            template=main_template,
+            files={'nested.yaml': nested_template},
+            environment=self.env).to_dict()
+
+        self._assert_results(result, stack_name)
+
+        # nested resources return a list of their resources.
+        res = result['resources'][0][0]
+        nested_stack_name = '%s-%s' % (stack_name,
+                                       res['parent_resource'])
+
+        self._assert_resource(res, nested_stack_name)
+        self.assertEqual('OS::Heat::TestResource',
+                         res['resource_type'])
+
+        self.assertEqual(False, res['properties']['fail'])
+        self.assertEqual(0, res['properties']['wait_secs'])
+        self.assertEqual(False, res['properties']['update_replace'])
+
+        self.assertEqual('abc', res['properties']['value'])
+        self.assertEqual([], res['required_by'])
diff --git a/functional/test_software_config.py b/functional/test_software_config.py
new file mode 100644
index 0000000..2e0ed76
--- /dev/null
+++ b/functional/test_software_config.py
@@ -0,0 +1,77 @@
+#    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 ParallelDeploymentsTest(test.HeatIntegrationTest):
+    template = '''
+heat_template_version: "2013-05-23"
+parameters:
+  flavor:
+    type: string
+  image:
+    type: string
+  network:
+    type: string
+resources:
+  server:
+    type: OS::Nova::Server
+    properties:
+      image: {get_param: image}
+      flavor: {get_param: flavor}
+      user_data_format: SOFTWARE_CONFIG
+      networks: [{network: {get_param: network} }]
+  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()
+        self.client = self.orchestration_client
+
+    def test_fail(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']))
diff --git a/functional/test_template_resource.py b/functional/test_template_resource.py
index 00e08b0..478cfae 100644
--- a/functional/test_template_resource.py
+++ b/functional/test_template_resource.py
@@ -687,3 +687,36 @@
         exp = 'Resource CREATE failed: ValueError: %s: %s' % (exp_path,
                                                               exp_msg)
         self.assertEqual(exp, stack.stack_status_reason)
+
+
+class TemplateResourceSuspendResumeTest(test.HeatIntegrationTest):
+    """Prove that we can do template resource suspend/resume."""
+
+    main_template = '''
+heat_template_version: 2014-10-16
+parameters:
+resources:
+  the_nested:
+    type: the.yaml
+'''
+
+    nested_templ = '''
+heat_template_version: 2014-10-16
+resources:
+  test_random_string:
+    type: OS::Heat::RandomString
+'''
+
+    def setUp(self):
+        super(TemplateResourceSuspendResumeTest, self).setUp()
+        self.client = self.orchestration_client
+
+    def test_suspend_resume(self):
+        """Basic test for template resource suspend resume"""
+        stack_identifier = self.stack_create(
+            template=self.main_template,
+            files={'the.yaml': self.nested_templ}
+        )
+
+        self.stack_suspend(stack_identifier=stack_identifier)
+        self.stack_resume(stack_identifier=stack_identifier)
diff --git a/scenario/templates/test_server_cfn_init.yaml b/scenario/templates/test_server_cfn_init.yaml
index ffb8e9b..9f94717 100644
--- a/scenario/templates/test_server_cfn_init.yaml
+++ b/scenario/templates/test_server_cfn_init.yaml
@@ -28,10 +28,16 @@
     Properties:
       UserName: {Ref: CfnUser}
 
-  IPAddress:
+  ElasticIp:
     Type: AWS::EC2::EIP
     Properties:
-      InstanceId: {Ref: SmokeServer}
+      Domain: vpc
+
+  SmokeServerElasticIp:
+    Type: AWS::EC2::EIPAssociation
+    Properties:
+       EIP: {Ref: ElasticIp}
+       InstanceId: {Ref: SmokeServer}
 
   SmokeServer:
     Type: AWS::EC2::Instance
@@ -81,7 +87,11 @@
     Description: Contents of /tmp/smoke-status on SmokeServer
     Value:
       Fn::GetAtt: [WaitCondition, Data]
-  SmokeServerIp:
-    Description: IP address of server
+  ElasticIp_Id:
+    Description: Elastic ip allocation id
     Value:
-      Ref: IPAddress
+      Fn::GetAtt: [ElasticIp, AllocationId]
+  SmokeServerElasticIp:
+    Description: Elastic ip address of server
+    Value:
+      Ref: ElasticIp
diff --git a/scenario/test_server_cfn_init.py b/scenario/test_server_cfn_init.py
index b5b1e67..267b44b 100644
--- a/scenario/test_server_cfn_init.py
+++ b/scenario/test_server_cfn_init.py
@@ -27,7 +27,7 @@
     def check_stack(self, sid):
         # Check status of all resources
         for res in ('WaitHandle', 'SmokeSecurityGroup', 'SmokeKeys',
-                    'CfnUser', 'SmokeServer', 'IPAddress'):
+                    'CfnUser', 'SmokeServer', 'SmokeServerElasticIp'):
             self._wait_for_resource_status(
                 sid, res, 'CREATE_COMPLETE')
 
@@ -59,7 +59,22 @@
             self._stack_output(stack, 'WaitConditionStatus'))
         self.assertEqual('smoke test complete', wait_status['smoke_status'])
 
-        server_ip = self._stack_output(stack, 'SmokeServerIp')
+        # Check EIP attributes.
+        server_floatingip_id = self._stack_output(stack,
+                                                  'ElasticIp_Id')
+        self.assertIsNotNone(server_floatingip_id)
+
+        # Fetch EIP details.
+        net_show = self.network_client.show_floatingip(
+            floatingip=server_floatingip_id)
+        floating_ip = net_show['floatingip']['floating_ip_address']
+        port_id = net_show['floatingip']['port_id']
+
+        # Ensure that EIP was assigned to server.
+        port_show = self.network_client.show_port(port=port_id)
+        self.assertEqual(server.id, port_show['port']['device_id'])
+        server_ip = self._stack_output(stack, 'SmokeServerElasticIp')
+        self.assertEqual(server_ip, floating_ip)
 
         # Check that created server is reachable
         if not self._ping_ip_address(server_ip):