From 20f17ec25b40ad9654c8ee6f6ce3d7294d44e02c Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Tue, 10 Feb 2015 13:54:46 +1300 Subject: [PATCH] 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 --- heat_integrationtests/common/config.py | 3 + heat_integrationtests/common/test.py | 3 +- .../heat_integrationtests.conf.sample | 3 + .../test_server_software_config.yaml | 174 ++++++++++++++++++ .../scenario/test_server_software_config.py | 158 ++++++++++++++++ 5 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 heat_integrationtests/scenario/templates/test_server_software_config.yaml create mode 100644 heat_integrationtests/scenario/test_server_software_config.py diff --git a/heat_integrationtests/common/config.py b/heat_integrationtests/common/config.py index ec7df2e8c..d4d41b0bf 100644 --- a/heat_integrationtests/common/config.py +++ b/heat_integrationtests/common/config.py @@ -85,6 +85,9 @@ IntegrationTestGroup = [ 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/heat_integrationtests/common/test.py b/heat_integrationtests/common/test.py index c8bce8b5f..02f7f8a4a 100644 --- a/heat_integrationtests/common/test.py +++ b/heat_integrationtests/common/test.py @@ -311,7 +311,8 @@ class HeatIntegrationTest(testscenarios.WithScenarios, 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/heat_integrationtests/heat_integrationtests.conf.sample b/heat_integrationtests/heat_integrationtests.conf.sample index 0319dab40..d887779dc 100644 --- a/heat_integrationtests/heat_integrationtests.conf.sample +++ b/heat_integrationtests/heat_integrationtests.conf.sample @@ -67,6 +67,9 @@ # The mask bits for tenant ipv4 subnets (integer value) #tenant_network_mask_bits = 28 +# Skip software config deployment tests (boolean value) +#skip_software_config_tests = true + # Default size in GB for volumes created by volumes tests (integer value) #volume_size = 1 diff --git a/heat_integrationtests/scenario/templates/test_server_software_config.yaml b/heat_integrationtests/scenario/templates/test_server_software_config.yaml new file mode 100644 index 000000000..e6ecae405 --- /dev/null +++ b/heat_integrationtests/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/heat_integrationtests/scenario/test_server_software_config.py b/heat_integrationtests/scenario/test_server_software_config.py new file mode 100644 index 000000000..bd5d18bd5 --- /dev/null +++ b/heat_integrationtests/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()