blob: 84789324154354150682a3401a1cabda3fd20ef9 [file] [log] [blame]
Angus Salkeldebf15d72014-12-10 17:03:15 +10001# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import logging
14
15from heat_integrationtests.common import test
16
17
18LOG = logging.getLogger(__name__)
19
20
21class InstanceGroupTest(test.HeatIntegrationTest):
22
23 template = '''
24{
25 "AWSTemplateFormatVersion" : "2010-09-09",
26 "Description" : "Template to create multiple instances.",
27 "Parameters" : {"size": {"Type": "String", "Default": "1"},
28 "AZ": {"Type": "String", "Default": "nova"},
29 "image": {"Type": "String"},
30 "flavor": {"Type": "String"},
31 "keyname": {"Type": "String"}},
32 "Resources": {
33 "JobServerGroup": {
34 "Type": "OS::Heat::InstanceGroup",
35 "Properties": {
36 "LaunchConfigurationName" : {"Ref": "JobServerConfig"},
37 "Size" : {"Ref": "size"},
38 "AvailabilityZones" : [{"Ref": "AZ"}]
39 }
40 },
41
42 "JobServerConfig" : {
43 "Type" : "AWS::AutoScaling::LaunchConfiguration",
44 "Metadata": {"foo": "bar"},
45 "Properties": {
46 "ImageId" : {"Ref": "image"},
47 "InstanceType" : {"Ref": "flavor"},
48 "KeyName" : {"Ref": "keyname"},
49 "SecurityGroups" : [ "sg-1" ],
Sergey Kraynev9612adc2014-12-19 08:17:08 -050050 "UserData" : "jsconfig data"
Angus Salkeldebf15d72014-12-10 17:03:15 +100051 }
52 }
53 },
54 "Outputs": {
55 "InstanceList": {"Value": {
56 "Fn::GetAtt": ["JobServerGroup", "InstanceList"]}}
57 }
58}
59'''
60
61 instance_template = '''
62heat_template_version: 2013-05-23
63parameters:
64 ImageId: {type: string}
65 InstanceType: {type: string}
66 KeyName: {type: string}
67 SecurityGroups: {type: comma_delimited_list}
68 UserData: {type: string}
69 Tags: {type: comma_delimited_list}
70
71resources:
72 random1:
73 type: OS::Heat::RandomString
74
75outputs:
76 PublicIp:
77 value: {get_attr: [random1, value]}
78'''
79
Angus Salkeldd67cf702014-12-18 10:40:47 +100080 # This is designed to fail.
81 bad_instance_template = '''
82heat_template_version: 2013-05-23
83parameters:
84 ImageId: {type: string}
85 InstanceType: {type: string}
86 KeyName: {type: string}
87 SecurityGroups: {type: comma_delimited_list}
88 UserData: {type: string}
89 Tags: {type: comma_delimited_list}
90
91resources:
92 random1:
93 type: OS::Heat::RandomString
94 depends_on: waiter
95 ready_poster:
96 type: AWS::CloudFormation::WaitConditionHandle
97 waiter:
98 type: AWS::CloudFormation::WaitCondition
99 properties:
100 Handle: {Ref: ready_poster}
101 Timeout: 1
102outputs:
103 PublicIp:
104 value: {get_attr: [random1, value]}
105'''
106
Angus Salkeldebf15d72014-12-10 17:03:15 +1000107 def setUp(self):
108 super(InstanceGroupTest, self).setUp()
109 self.client = self.orchestration_client
110 if not self.conf.image_ref:
111 raise self.skipException("No image configured to test")
112 if not self.conf.keypair_name:
113 raise self.skipException("No keyname configured to test")
114 if not self.conf.instance_type:
115 raise self.skipException("No flavor configured to test")
116
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000117 def assert_instance_count(self, stack, expected_count):
118 inst_list = self._stack_output(stack, 'InstanceList')
119 self.assertEqual(expected_count, len(inst_list.split(',')))
120
Angus Salkeldd67cf702014-12-18 10:40:47 +1000121 def _get_nested_identifier(self, stack_identifier):
122 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
123 nested_link = [l for l in rsrc.links if l['rel'] == 'nested']
124 self.assertEqual(1, len(nested_link))
125 nested_href = nested_link[0]['href']
126 nested_id = nested_href.split('/')[-1]
127 nested_identifier = '/'.join(nested_href.split('/')[-2:])
128 physical_resource_id = rsrc.physical_resource_id
129 self.assertEqual(physical_resource_id, nested_id)
130 return nested_identifier
131
132 def _assert_instance_state(self, nested_identifier,
133 num_complete, num_failed):
134 for res in self.client.resources.list(nested_identifier):
135 if 'COMPLETE' in res.resource_status:
136 num_complete = num_complete - 1
137 elif 'FAILED' in res.resource_status:
138 num_failed = num_failed - 1
139 self.assertEqual(0, num_failed)
140 self.assertEqual(0, num_complete)
141
Angus Salkeldebf15d72014-12-10 17:03:15 +1000142 def test_basic_create_works(self):
143 """Make sure the working case is good.
144 Note this combines test_override_aws_ec2_instance into this test as
145 well, which is:
146 If AWS::EC2::Instance is overridden, InstanceGroup will automatically
147 use that overridden resource type.
148 """
149
Angus Salkeldebf15d72014-12-10 17:03:15 +1000150 files = {'provider.yaml': self.instance_template}
151 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
152 'parameters': {'size': 4,
153 'image': self.conf.image_ref,
154 'keyname': self.conf.keypair_name,
155 'flavor': self.conf.instance_type}}
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000156 stack_identifier = self.stack_create(template=self.template,
157 files=files, environment=env)
Angus Salkeldebf15d72014-12-10 17:03:15 +1000158 initial_resources = {
159 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
160 'JobServerGroup': 'OS::Heat::InstanceGroup'}
161 self.assertEqual(initial_resources,
162 self.list_resources(stack_identifier))
163
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000164 stack = self.client.stacks.get(stack_identifier)
165 self.assert_instance_count(stack, 4)
166
167 def test_size_updates_work(self):
168 files = {'provider.yaml': self.instance_template}
169 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
170 'parameters': {'size': 2,
171 'image': self.conf.image_ref,
172 'keyname': self.conf.keypair_name,
173 'flavor': self.conf.instance_type}}
174
175 stack_identifier = self.stack_create(template=self.template,
176 files=files,
177 environment=env)
178 stack = self.client.stacks.get(stack_identifier)
179 self.assert_instance_count(stack, 2)
180
181 # Increase min size to 5
182 env2 = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
183 'parameters': {'size': 5,
184 'image': self.conf.image_ref,
185 'keyname': self.conf.keypair_name,
186 'flavor': self.conf.instance_type}}
187 self.update_stack(stack_identifier, self.template,
188 environment=env2, files=files)
189 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
190 stack = self.client.stacks.get(stack_identifier)
191 self.assert_instance_count(stack, 5)
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000192
193 def test_update_group_replace(self):
194 """Make sure that during a group update the non updatable
195 properties cause a replacement.
196 """
197 files = {'provider.yaml': self.instance_template}
198 env = {'resource_registry':
199 {'AWS::EC2::Instance': 'provider.yaml'},
200 'parameters': {'size': 1,
201 'image': self.conf.image_ref,
202 'keyname': self.conf.keypair_name,
203 'flavor': self.conf.instance_type}}
204
205 stack_identifier = self.stack_create(template=self.template,
206 files=files,
207 environment=env)
208 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
209 orig_asg_id = rsrc.physical_resource_id
210
211 env2 = {'resource_registry':
212 {'AWS::EC2::Instance': 'provider.yaml'},
213 'parameters': {'size': '2',
214 'AZ': 'wibble',
215 'image': self.conf.image_ref,
216 'keyname': self.conf.keypair_name,
217 'flavor': self.conf.instance_type}}
218 self.update_stack(stack_identifier, self.template,
219 environment=env2, files=files)
220
221 # replacement will cause the resource physical_resource_id to change.
222 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
223 self.assertNotEqual(orig_asg_id, rsrc.physical_resource_id)
Angus Salkeldd67cf702014-12-18 10:40:47 +1000224
225 def test_create_instance_error_causes_group_error(self):
226 """If a resource in an instance group fails to be created, the instance
227 group itself will fail and the broken inner resource will remain.
228 """
229 stack_name = self._stack_rand_name()
230 files = {'provider.yaml': self.bad_instance_template}
231 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
232 'parameters': {'size': 2,
233 'image': self.conf.image_ref,
234 'keyname': self.conf.keypair_name,
235 'flavor': self.conf.instance_type}}
236
237 self.client.stacks.create(
238 stack_name=stack_name,
239 template=self.template,
240 files=files,
241 disable_rollback=True,
242 parameters={},
243 environment=env
244 )
245 self.addCleanup(self.client.stacks.delete, stack_name)
246 stack = self.client.stacks.get(stack_name)
247 stack_identifier = '%s/%s' % (stack_name, stack.id)
248 self._wait_for_stack_status(stack_identifier, 'CREATE_FAILED')
249 initial_resources = {
250 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
251 'JobServerGroup': 'OS::Heat::InstanceGroup'}
252 self.assertEqual(initial_resources,
253 self.list_resources(stack_identifier))
254
255 nested_ident = self._get_nested_identifier(stack_identifier)
256 self._assert_instance_state(nested_ident, 0, 2)
257
258 def test_update_instance_error_causes_group_error(self):
259 """If a resource in an instance group fails to be created during an
260 update, the instance group itself will fail and the broken inner
261 resource will remain.
262 """
263 files = {'provider.yaml': self.instance_template}
264 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
265 'parameters': {'size': 2,
266 'image': self.conf.image_ref,
267 'keyname': self.conf.keypair_name,
268 'flavor': self.conf.instance_type}}
269
270 stack_identifier = self.stack_create(template=self.template,
271 files=files,
272 environment=env)
273 initial_resources = {
274 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
275 'JobServerGroup': 'OS::Heat::InstanceGroup'}
276 self.assertEqual(initial_resources,
277 self.list_resources(stack_identifier))
278
279 stack = self.client.stacks.get(stack_identifier)
280 self.assert_instance_count(stack, 2)
281 nested_ident = self._get_nested_identifier(stack_identifier)
282 self._assert_instance_state(nested_ident, 2, 0)
283
284 env['parameters']['size'] = 3
285 files2 = {'provider.yaml': self.bad_instance_template}
286 self.client.stacks.update(
287 stack_id=stack_identifier,
288 template=self.template,
289 files=files2,
290 disable_rollback=True,
291 parameters={},
292 environment=env
293 )
294 self._wait_for_stack_status(stack_identifier, 'UPDATE_FAILED')
295
296 # assert that there are 3 bad instances
297 nested_ident = self._get_nested_identifier(stack_identifier)
298 self._assert_instance_state(nested_ident, 0, 3)