# 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 copy import json from heatclient import exc import six import yaml from heat_integrationtests.common import test class ResourceGroupTest(test.HeatIntegrationTest): template = ''' heat_template_version: 2013-05-23 resources: random_group: type: OS::Heat::ResourceGroup properties: count: 0 resource_def: type: My::RandomString properties: length: 30 salt: initial 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]} ''' def setUp(self): super(ResourceGroupTest, self).setUp() self.client = self.orchestration_client def _group_nested_identifier(self, stack_identifier, group_name='random_group'): # Get the nested stack identifier from the group rsrc = self.client.resources.get(stack_identifier, group_name) physical_resource_id = rsrc.physical_resource_id nested_stack = self.client.stacks.get(physical_resource_id) nested_identifier = '%s/%s' % (nested_stack.stack_name, nested_stack.id) parent_id = stack_identifier.split("/")[-1] self.assertEqual(parent_id, nested_stack.parent) return nested_identifier def test_resource_group_zero_novalidate(self): # Nested resources should be validated only when size > 0 # This allows features to be disabled via size=0 without # triggering validation of nested resource custom contraints # e.g images etc in the nested schema. nested_template_fail = ''' heat_template_version: 2013-05-23 parameters: length: type: string default: 50 salt: type: string default: initial resources: random: type: OS::Heat::RandomString properties: length: BAD ''' files = {'provider.yaml': nested_template_fail} env = {'resource_registry': {'My::RandomString': 'provider.yaml'}} stack_identifier = self.stack_create( template=self.template, files=files, environment=env ) self.assertEqual({u'random_group': u'OS::Heat::ResourceGroup'}, self.list_resources(stack_identifier)) # Check we created an empty nested stack nested_identifier = self._group_nested_identifier(stack_identifier) self.assertEqual({}, self.list_resources(nested_identifier)) # Prove validation works for non-zero create/update template_two_nested = self.template.replace("count: 0", "count: 2") expected_err = "length Value 'BAD' is not an integer" ex = self.assertRaises(exc.HTTPBadRequest, self.update_stack, stack_identifier, template_two_nested, environment=env, files=files) self.assertIn(expected_err, six.text_type(ex)) ex = self.assertRaises(exc.HTTPBadRequest, self.stack_create, 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), 'My::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. env = {'resource_registry': {'My::RandomString': 'OS::Heat::RandomString'}} create_template = self.template.replace("count: 0", "count: 2") stack_identifier = self.stack_create(template=create_template, environment=env) 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 env = {'resource_registry': {'My::RandomString': 'OS::Heat::RandomString'}} create_template = self.template.replace("count: 0", "count: 2") stack_identifier = self.stack_create(template=create_template, environment=env) 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 = self.template.replace("count: 0", "count: 5") self.update_stack(stack_identifier, update_template, environment=env) # verify that the resource group has 5 resources self._validate_resources(stack_identifier, 5) # decrease the resource group count to 3 update_template = self.template.replace("count: 0", "count: 3") self.update_stack(stack_identifier, update_template, environment=env) # verify that the resource group has 3 resources self._validate_resources(stack_identifier, 3) def test_props_update(self): """Test update of resource_def properties behaves as expected.""" env = {'resource_registry': {'My::RandomString': 'OS::Heat::RandomString'}} template_one = self.template.replace("count: 0", "count: 1") stack_identifier = self.stack_create(template=template_one, environment=env) self.assertEqual({u'random_group': u'OS::Heat::ResourceGroup'}, self.list_resources(stack_identifier)) initial_nested_ident = self._group_nested_identifier(stack_identifier) self.assertEqual({'0': 'My::RandomString'}, self.list_resources(initial_nested_ident)) # get the resource id res = self.client.resources.get(initial_nested_ident, '0') initial_res_id = res.physical_resource_id # change the salt (this should replace the RandomString but # not the nested stack or resource group. template_salt = template_one.replace("salt: initial", "salt: more") self.update_stack(stack_identifier, template_salt, environment=env) updated_nested_ident = self._group_nested_identifier(stack_identifier) self.assertEqual(initial_nested_ident, updated_nested_ident) # compare the resource id, we expect a change. res = self.client.resources.get(updated_nested_ident, '0') updated_res_id = res.physical_resource_id self.assertNotEqual(initial_res_id, updated_res_id) def test_update_nochange(self): """Test update with no properties change.""" env = {'resource_registry': {'My::RandomString': 'OS::Heat::RandomString'}} template_one = self.template.replace("count: 0", "count: 1") stack_identifier = self.stack_create(template=template_one, environment=env) self.assertEqual({u'random_group': u'OS::Heat::ResourceGroup'}, self.list_resources(stack_identifier)) initial_nested_ident = self._group_nested_identifier(stack_identifier) self.assertEqual({'0': 'My::RandomString'}, self.list_resources(initial_nested_ident)) # get the output stack0 = self.client.stacks.get(stack_identifier) initial_rand = self._stack_output(stack0, 'random1') template_copy = copy.deepcopy(template_one) self.update_stack(stack_identifier, template_copy, environment=env) updated_nested_ident = self._group_nested_identifier(stack_identifier) self.assertEqual(initial_nested_ident, updated_nested_ident) # compare the random number, we expect no change. stack1 = self.client.stacks.get(stack_identifier) updated_rand = self._stack_output(stack1, 'random1') self.assertEqual(initial_rand, updated_rand) def test_update_nochange_resource_needs_update(self): """Test update when the resource definition has changed.""" # Test the scenario when the ResourceGroup update happens without # any changed properties, this can happen if the definition of # a contained provider resource changes (files map changes), then # the group and underlying nested stack should end up updated. random_templ1 = ''' heat_template_version: 2013-05-23 parameters: length: type: string default: not-used salt: type: string default: not-used resources: random1: type: OS::Heat::RandomString properties: salt: initial outputs: value: value: {get_attr: [random1, value]} ''' files1 = {'my_random.yaml': random_templ1} random_templ2 = random_templ1.replace('salt: initial', 'salt: more') files2 = {'my_random.yaml': random_templ2} env = {'resource_registry': {'My::RandomString': 'my_random.yaml'}} template_one = self.template.replace("count: 0", "count: 1") stack_identifier = self.stack_create(template=template_one, environment=env, files=files1) self.assertEqual({u'random_group': u'OS::Heat::ResourceGroup'}, self.list_resources(stack_identifier)) initial_nested_ident = self._group_nested_identifier(stack_identifier) self.assertEqual({'0': 'My::RandomString'}, self.list_resources(initial_nested_ident)) # get the output stack0 = self.client.stacks.get(stack_identifier) initial_rand = self._stack_output(stack0, 'random1') # change the environment so we use a different TemplateResource. # note "files2". self.update_stack(stack_identifier, template_one, environment=env, files=files2) updated_nested_ident = self._group_nested_identifier(stack_identifier) self.assertEqual(initial_nested_ident, updated_nested_ident) # compare the output, we expect a change. stack1 = self.client.stacks.get(stack_identifier) updated_rand = self._stack_output(stack1, 'random1') self.assertNotEqual(initial_rand, updated_rand) class ResourceGroupTestNullParams(test.HeatIntegrationTest): template = ''' heat_template_version: 2013-05-23 parameters: param: type: empty resources: random_group: type: OS::Heat::ResourceGroup properties: count: 1 resource_def: type: My::RandomString properties: param: {get_param: param} outputs: val: value: {get_attr: [random_group, val]} ''' nested_template_file = ''' heat_template_version: 2013-05-23 parameters: param: type: empty outputs: val: value: {get_param: param} ''' scenarios = [ ('string_empty', dict( param='', p_type='string', )), ('boolean_false', dict( param=False, p_type='boolean', )), ('number_zero', dict( param=0, p_type='number', )), ('comma_delimited_list', dict( param=[], p_type='comma_delimited_list', )), ('json_empty', dict( param={}, p_type='json', )), ] def setUp(self): super(ResourceGroupTestNullParams, self).setUp() self.client = self.orchestration_client def test_create_pass_zero_parameter(self): templ = self.template.replace('type: empty', 'type: %s' % self.p_type) n_t_f = self.nested_template_file.replace('type: empty', 'type: %s' % self.p_type) files = {'provider.yaml': n_t_f} env = {'resource_registry': {'My::RandomString': 'provider.yaml'}} stack_identifier = self.stack_create( template=templ, files=files, environment=env, parameters={'param': self.param} ) stack = self.client.stacks.get(stack_identifier) self.assertEqual(self.param, self._stack_output(stack, 'val')[0]) class ResourceGroupAdoptTest(test.HeatIntegrationTest): """Prove that we can do resource group adopt.""" main_template = ''' heat_template_version: "2013-05-23" resources: group1: type: OS::Heat::ResourceGroup properties: count: 2 resource_def: type: OS::Heat::RandomString outputs: test0: value: {get_attr: [group1, resource.0.value]} test1: value: {get_attr: [group1, resource.1.value]} ''' def setUp(self): super(ResourceGroupAdoptTest, self).setUp() self.client = self.orchestration_client def _yaml_to_json(self, yaml_templ): return yaml.load(yaml_templ) def test_adopt(self): data = { "resources": { "group1": { "status": "COMPLETE", "name": "group1", "resource_data": {}, "metadata": {}, "resource_id": "test-group1-id", "action": "CREATE", "type": "OS::Heat::ResourceGroup", "resources": { "0": { "status": "COMPLETE", "name": "0", "resource_data": {"value": "goopie"}, "resource_id": "ID-0", "action": "CREATE", "type": "OS::Heat::RandomString", "metadata": {} }, "1": { "status": "COMPLETE", "name": "1", "resource_data": {"value": "different"}, "resource_id": "ID-1", "action": "CREATE", "type": "OS::Heat::RandomString", "metadata": {} } } } }, "environment": {"parameters": {}}, "template": yaml.load(self.main_template) } stack_identifier = self.stack_adopt( adopt_data=json.dumps(data)) self.assert_resource_is_a_stack(stack_identifier, 'group1') stack = self.client.stacks.get(stack_identifier) self.assertEqual('goopie', self._stack_output(stack, 'test0')) self.assertEqual('different', self._stack_output(stack, 'test1'))