Integration test for software-config tools

This test currently exercises the following hooks:
- script
- puppet
- cfn-init
This requires devstack building a custom image. Since gating doesn't
yet have a test image available the test is skipped by default via
config value skip_software_config_tests.

To run this test locally, build your own custom image and set
skip_software_config_tests=false in
heat_integrationtests/heat_integrationtests.conf

Change-Id: I9d27664638de95e52bc954e1fa00299e6711de90
diff --git a/common/config.py b/common/config.py
index ec7df2e..d4d41b0 100644
--- a/common/config.py
+++ b/common/config.py
@@ -85,6 +85,9 @@
     cfg.IntOpt('tenant_network_mask_bits',
                default=28,
                help="The mask bits for tenant ipv4 subnets"),
+    cfg.BoolOpt('skip_software_config_tests',
+                default=True,
+                help="Skip software config deployment tests"),
     cfg.IntOpt('volume_size',
                default=1,
                help='Default size in GB for volumes created by volumes tests'),
diff --git a/common/test.py b/common/test.py
index c8bce8b..02f7f8a 100644
--- a/common/test.py
+++ b/common/test.py
@@ -311,7 +311,8 @@
 
         stack = self.client.stacks.get(name)
         stack_identifier = '%s/%s' % (name, stack.id)
-        self._wait_for_stack_status(stack_identifier, expected_status)
+        if expected_status:
+            self._wait_for_stack_status(stack_identifier, expected_status)
         return stack_identifier
 
     def stack_adopt(self, stack_name=None, files=None,
diff --git a/scenario/templates/test_server_software_config.yaml b/scenario/templates/test_server_software_config.yaml
new file mode 100644
index 0000000..e6ecae4
--- /dev/null
+++ b/scenario/templates/test_server_software_config.yaml
@@ -0,0 +1,174 @@
+heat_template_version: 2014-10-16
+parameters:
+  key_name:
+    type: string
+  flavor:
+    type: string
+  image:
+    type: string
+  network:
+    type: string
+  signal_transport:
+    type: string
+    default: CFN_SIGNAL
+  dep1_foo:
+    default: fooooo
+    type: string
+  dep1_bar:
+    default: baaaaa
+    type: string
+  dep2a_bar:
+    type: string
+    default: barrr
+  dep3_foo:
+    default: fo
+    type: string
+  dep3_bar:
+    default: ba
+    type: string
+
+resources:
+
+  the_sg:
+    type: OS::Neutron::SecurityGroup
+    properties:
+      name: the_sg
+      description: Ping and SSH
+      rules:
+      - protocol: icmp
+      - protocol: tcp
+        port_range_min: 22
+        port_range_max: 22
+
+  cfg1:
+    type: OS::Heat::SoftwareConfig
+    properties:
+      group: script
+      inputs:
+      - name: foo
+      - name: bar
+      outputs:
+      - name: result
+      config: {get_file: cfg1.sh}
+
+  cfg2a:
+    type: OS::Heat::StructuredConfig
+    properties:
+      group: cfn-init
+      inputs:
+      - name: bar
+      config:
+        config:
+          files:
+            /tmp/cfn-init-foo:
+              content:
+                get_input: bar
+              mode: '000644'
+
+  cfg2b:
+    type: OS::Heat::SoftwareConfig
+    properties:
+      group: script
+      outputs:
+      - name: result
+      config: |
+        #!/bin/sh
+        echo -n "The file /tmp/cfn-init-foo contains `cat /tmp/cfn-init-foo` for server $deploy_server_id during $deploy_action" > $heat_outputs_path.result
+
+  cfg3:
+    type: OS::Heat::SoftwareConfig
+    properties:
+      group: puppet
+      inputs:
+      - name: foo
+      - name: bar
+      outputs:
+      - name: result
+      config: {get_file: cfg3.pp}
+
+  dep1:
+    type: OS::Heat::SoftwareDeployment
+    properties:
+      config:
+        get_resource: cfg1
+      server:
+        get_resource: server
+      input_values:
+        foo: {get_param: dep1_foo}
+        bar: {get_param: dep1_bar}
+      signal_transport: {get_param: signal_transport}
+
+  dep2a:
+    type: OS::Heat::StructuredDeployment
+    properties:
+      name: 10_dep2a
+      signal_transport: NO_SIGNAL
+      config:
+        get_resource: cfg2a
+      server:
+        get_resource: server
+      input_values:
+        bar: {get_param: dep2a_bar}
+
+  dep2b:
+    type: OS::Heat::SoftwareDeployment
+    properties:
+      name: 20_dep2b
+      config:
+        get_resource: cfg2b
+      server:
+        get_resource: server
+      signal_transport: {get_param: signal_transport}
+
+  dep3:
+    type: OS::Heat::SoftwareDeployment
+    properties:
+      config:
+        get_resource: cfg3
+      server:
+        get_resource: server
+      input_values:
+        foo: {get_param: dep3_foo}
+        bar: {get_param: dep3_bar}
+      signal_transport: {get_param: signal_transport}
+
+  cfg_user_data:
+    type: OS::Heat::SoftwareConfig
+    properties:
+      config: |
+        #!/bin/sh
+        echo "user data script"
+
+  server:
+    type: OS::Nova::Server
+    properties:
+      image: {get_param: image}
+      flavor: {get_param: flavor}
+      key_name: {get_param: key_name}
+      security_groups:
+      - {get_resource: the_sg}
+      networks:
+      - network: {get_param: network}
+      user_data_format: SOFTWARE_CONFIG
+      software_config_transport: POLL_TEMP_URL
+      user_data: {get_resource: cfg_user_data}
+
+outputs:
+  res1:
+    value:
+      result: {get_attr: [dep1, result]}
+      stdout: {get_attr: [dep1, deploy_stdout]}
+      stderr: {get_attr: [dep1, deploy_stderr]}
+      status_code: {get_attr: [dep1, deploy_status_code]}
+  res2:
+    value:
+      result: {get_attr: [dep2b, result]}
+      stdout: {get_attr: [dep2b, deploy_stdout]}
+      stderr: {get_attr: [dep2b, deploy_stderr]}
+      status_code: {get_attr: [dep2b, deploy_status_code]}
+  res3:
+    value:
+      result: {get_attr: [dep3, result]}
+      stdout: {get_attr: [dep3, deploy_stdout]}
+      stderr: {get_attr: [dep3, deploy_stderr]}
+      status_code: {get_attr: [dep3, deploy_status_code]}
diff --git a/scenario/test_server_software_config.py b/scenario/test_server_software_config.py
new file mode 100644
index 0000000..bd5d18b
--- /dev/null
+++ b/scenario/test_server_software_config.py
@@ -0,0 +1,158 @@
+#    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 six
+
+from heat_integrationtests.common import exceptions
+from heat_integrationtests.common import test
+
+CFG1_SH = '''#!/bin/sh
+echo "Writing to /tmp/$bar"
+echo $foo > /tmp/$bar
+echo -n "The file /tmp/$bar contains `cat /tmp/$bar` for server \
+$deploy_server_id during $deploy_action" > $heat_outputs_path.result
+echo "Written to /tmp/$bar"
+echo "Output to stderr" 1>&2
+'''
+
+CFG3_PP = '''file {'barfile':
+  ensure  => file,
+  mode    => 0644,
+  path    => "/tmp/$::bar",
+  content => "$::foo",
+}
+file {'output_result':
+  ensure  => file,
+  path    => "$::heat_outputs_path.result",
+  mode    => 0644,
+  content => "The file /tmp/$::bar contains $::foo for server \
+$::deploy_server_id during $::deploy_action",
+}'''
+
+
+class SoftwareConfigIntegrationTest(test.HeatIntegrationTest):
+
+    def setUp(self):
+        super(SoftwareConfigIntegrationTest, self).setUp()
+        if self.conf.skip_software_config_tests:
+            self.skipTest('Testing software config disabled in conf, '
+                          'skipping')
+        self.client = self.orchestration_client
+        self.template_name = 'test_server_software_config.yaml'
+        self.sub_dir = 'templates'
+        self.stack_name = self._stack_rand_name()
+        self.maxDiff = None
+
+    def launch_stack(self):
+        net = self._get_default_network()
+        self.parameters = {
+            'key_name': self.keypair_name,
+            'flavor': self.conf.instance_type,
+            'image': self.conf.image_ref,
+            'network': net['id']
+        }
+
+        # create the stack
+        self.template = self._load_template(__file__, self.template_name,
+                                            self.sub_dir)
+        self.stack_create(
+            stack_name=self.stack_name,
+            template=self.template,
+            parameters=self.parameters,
+            files={
+                'cfg1.sh': CFG1_SH,
+                'cfg3.pp': CFG3_PP
+            },
+            expected_status=None)
+
+        self.stack = self.client.stacks.get(self.stack_name)
+        self.stack_identifier = '%s/%s' % (self.stack_name, self.stack.id)
+
+    def check_stack(self):
+        sid = self.stack_identifier
+        for res in ('cfg2a', 'cfg2b', 'cfg1', 'cfg3', 'server'):
+            self._wait_for_resource_status(
+                sid, res, 'CREATE_COMPLETE')
+
+        server_resource = self.client.resources.get(sid, 'server')
+        server_id = server_resource.physical_resource_id
+        server = self.compute_client.servers.get(server_id)
+
+        try:
+            # wait for each deployment to contribute their
+            # config to resource
+            for res in ('dep2b', 'dep1', 'dep3'):
+                self._wait_for_resource_status(
+                    sid, res, 'CREATE_IN_PROGRESS')
+
+            server_metadata = self.client.resources.metadata(sid, 'server')
+            deployments = dict((d['name'], d) for d in
+                               server_metadata['deployments'])
+
+            for res in ('dep2a', 'dep2b', 'dep1', 'dep3'):
+                self._wait_for_resource_status(
+                    sid, res, 'CREATE_COMPLETE')
+        except (exceptions.StackResourceBuildErrorException,
+                exceptions.TimeoutException) as e:
+            self._log_console_output(servers=[server])
+            raise e
+
+        self._wait_for_stack_status(sid, 'CREATE_COMPLETE')
+
+        complete_server_metadata = self.client.resources.metadata(
+            sid, 'server')
+        # ensure any previously available deployments haven't changed so
+        # config isn't re-triggered
+        complete_deployments = dict((d['name'], d) for d in
+                                    complete_server_metadata['deployments'])
+        for k, v in six.iteritems(deployments):
+            self.assertEqual(v, complete_deployments[k])
+
+        stack = self.client.stacks.get(sid)
+
+        res1 = self._stack_output(stack, 'res1')
+        self.assertEqual(
+            'The file %s contains %s for server %s during %s' % (
+                '/tmp/baaaaa', 'fooooo', server_id, 'CREATE'),
+            res1['result'])
+        self.assertEqual(0, res1['status_code'])
+        self.assertEqual('Output to stderr\n', res1['stderr'])
+        self.assertTrue(len(res1['stdout']) > 0)
+
+        res2 = self._stack_output(stack, 'res2')
+        self.assertEqual(
+            'The file %s contains %s for server %s during %s' % (
+                '/tmp/cfn-init-foo', 'barrr', server_id, 'CREATE'),
+            res2['result'])
+        self.assertEqual(0, res2['status_code'])
+        self.assertEqual('', res2['stderr'])
+        self.assertEqual('', res2['stdout'])
+
+        res3 = self._stack_output(stack, 'res3')
+        self.assertEqual(
+            'The file %s contains %s for server %s during %s' % (
+                '/tmp/ba', 'fo', server_id, 'CREATE'),
+            res3['result'])
+        self.assertEqual(0, res3['status_code'])
+        self.assertEqual('', res3['stderr'])
+        self.assertTrue(len(res1['stdout']) > 0)
+
+        dep1_resource = self.client.resources.get(sid, 'dep1')
+        dep1_id = dep1_resource.physical_resource_id
+        dep1_dep = self.client.software_deployments.get(dep1_id)
+        self.assertIsNotNone(dep1_dep.updated_time)
+        self.assertNotEqual(dep1_dep.updated_time, dep1_dep.creation_time)
+
+    def test_server_software_config(self):
+        self.assign_keypair()
+        self.launch_stack()
+        self.check_stack()