blob: a8494c29c905807ee368b88885691c88764b1dbc [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
Angus Salkeld771235a2015-01-20 15:11:42 +100013import copy
14import json
Angus Salkeldebf15d72014-12-10 17:03:15 +100015
Angus Salkeld771235a2015-01-20 15:11:42 +100016from testtools import matchers
17
Rabi Mishra477efc92015-07-31 13:01:45 +053018from heat_integrationtests.functional import functional_base
Angus Salkeldebf15d72014-12-10 17:03:15 +100019
20
Rabi Mishra477efc92015-07-31 13:01:45 +053021class InstanceGroupTest(functional_base.FunctionalTestsBase):
Angus Salkeldebf15d72014-12-10 17:03:15 +100022
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"},
Anastasia Kuznetsova33258742015-01-14 16:13:42 +040030 "flavor": {"Type": "String"}},
Angus Salkeldebf15d72014-12-10 17:03:15 +100031 "Resources": {
32 "JobServerGroup": {
33 "Type": "OS::Heat::InstanceGroup",
34 "Properties": {
35 "LaunchConfigurationName" : {"Ref": "JobServerConfig"},
36 "Size" : {"Ref": "size"},
37 "AvailabilityZones" : [{"Ref": "AZ"}]
38 }
39 },
40
41 "JobServerConfig" : {
42 "Type" : "AWS::AutoScaling::LaunchConfiguration",
43 "Metadata": {"foo": "bar"},
44 "Properties": {
45 "ImageId" : {"Ref": "image"},
46 "InstanceType" : {"Ref": "flavor"},
Angus Salkeldebf15d72014-12-10 17:03:15 +100047 "SecurityGroups" : [ "sg-1" ],
Sergey Kraynev9612adc2014-12-19 08:17:08 -050048 "UserData" : "jsconfig data"
Angus Salkeldebf15d72014-12-10 17:03:15 +100049 }
50 }
51 },
52 "Outputs": {
53 "InstanceList": {"Value": {
Angus Salkeld771235a2015-01-20 15:11:42 +100054 "Fn::GetAtt": ["JobServerGroup", "InstanceList"]}},
55 "JobServerConfigRef": {"Value": {
56 "Ref": "JobServerConfig"}}
Angus Salkeldebf15d72014-12-10 17:03:15 +100057 }
58}
59'''
60
61 instance_template = '''
62heat_template_version: 2013-05-23
63parameters:
64 ImageId: {type: string}
65 InstanceType: {type: string}
Angus Salkeldebf15d72014-12-10 17:03:15 +100066 SecurityGroups: {type: comma_delimited_list}
67 UserData: {type: string}
68 Tags: {type: comma_delimited_list}
69
70resources:
71 random1:
72 type: OS::Heat::RandomString
Angus Salkeld771235a2015-01-20 15:11:42 +100073 properties:
74 salt: {get_param: ImageId}
Angus Salkeldebf15d72014-12-10 17:03:15 +100075outputs:
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}
Angus Salkeldd67cf702014-12-18 10:40:47 +100086 SecurityGroups: {type: comma_delimited_list}
87 UserData: {type: string}
88 Tags: {type: comma_delimited_list}
89
90resources:
91 random1:
92 type: OS::Heat::RandomString
93 depends_on: waiter
94 ready_poster:
95 type: AWS::CloudFormation::WaitConditionHandle
96 waiter:
97 type: AWS::CloudFormation::WaitCondition
98 properties:
99 Handle: {Ref: ready_poster}
100 Timeout: 1
101outputs:
102 PublicIp:
103 value: {get_attr: [random1, value]}
104'''
105
Angus Salkeldebf15d72014-12-10 17:03:15 +1000106 def setUp(self):
107 super(InstanceGroupTest, self).setUp()
Angus Salkeldebf15d72014-12-10 17:03:15 +1000108 if not self.conf.image_ref:
109 raise self.skipException("No image configured to test")
Angus Salkeld771235a2015-01-20 15:11:42 +1000110 if not self.conf.minimal_image_ref:
111 raise self.skipException("No minimal image configured to test")
Angus Salkeldebf15d72014-12-10 17:03:15 +1000112 if not self.conf.instance_type:
113 raise self.skipException("No flavor configured to test")
114
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000115 def assert_instance_count(self, stack, expected_count):
116 inst_list = self._stack_output(stack, 'InstanceList')
117 self.assertEqual(expected_count, len(inst_list.split(',')))
118
Angus Salkeldd67cf702014-12-18 10:40:47 +1000119 def _assert_instance_state(self, nested_identifier,
120 num_complete, num_failed):
121 for res in self.client.resources.list(nested_identifier):
122 if 'COMPLETE' in res.resource_status:
123 num_complete = num_complete - 1
124 elif 'FAILED' in res.resource_status:
125 num_failed = num_failed - 1
126 self.assertEqual(0, num_failed)
127 self.assertEqual(0, num_complete)
128
Angus Salkeld771235a2015-01-20 15:11:42 +1000129
130class InstanceGroupBasicTest(InstanceGroupTest):
131
Angus Salkeldebf15d72014-12-10 17:03:15 +1000132 def test_basic_create_works(self):
133 """Make sure the working case is good.
134 Note this combines test_override_aws_ec2_instance into this test as
135 well, which is:
136 If AWS::EC2::Instance is overridden, InstanceGroup will automatically
137 use that overridden resource type.
138 """
139
Angus Salkeldebf15d72014-12-10 17:03:15 +1000140 files = {'provider.yaml': self.instance_template}
141 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
142 'parameters': {'size': 4,
143 'image': self.conf.image_ref,
Angus Salkeldebf15d72014-12-10 17:03:15 +1000144 'flavor': self.conf.instance_type}}
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000145 stack_identifier = self.stack_create(template=self.template,
146 files=files, environment=env)
Angus Salkeldebf15d72014-12-10 17:03:15 +1000147 initial_resources = {
148 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
149 'JobServerGroup': 'OS::Heat::InstanceGroup'}
150 self.assertEqual(initial_resources,
151 self.list_resources(stack_identifier))
152
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000153 stack = self.client.stacks.get(stack_identifier)
154 self.assert_instance_count(stack, 4)
155
156 def test_size_updates_work(self):
157 files = {'provider.yaml': self.instance_template}
158 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
159 'parameters': {'size': 2,
160 'image': self.conf.image_ref,
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000161 'flavor': self.conf.instance_type}}
162
163 stack_identifier = self.stack_create(template=self.template,
164 files=files,
165 environment=env)
166 stack = self.client.stacks.get(stack_identifier)
167 self.assert_instance_count(stack, 2)
168
169 # Increase min size to 5
170 env2 = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
171 'parameters': {'size': 5,
172 'image': self.conf.image_ref,
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000173 'flavor': self.conf.instance_type}}
174 self.update_stack(stack_identifier, self.template,
175 environment=env2, files=files)
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000176 stack = self.client.stacks.get(stack_identifier)
177 self.assert_instance_count(stack, 5)
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000178
179 def test_update_group_replace(self):
180 """Make sure that during a group update the non updatable
181 properties cause a replacement.
182 """
183 files = {'provider.yaml': self.instance_template}
184 env = {'resource_registry':
185 {'AWS::EC2::Instance': 'provider.yaml'},
186 'parameters': {'size': 1,
187 'image': self.conf.image_ref,
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000188 'flavor': self.conf.instance_type}}
189
190 stack_identifier = self.stack_create(template=self.template,
191 files=files,
192 environment=env)
193 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
194 orig_asg_id = rsrc.physical_resource_id
195
196 env2 = {'resource_registry':
197 {'AWS::EC2::Instance': 'provider.yaml'},
198 'parameters': {'size': '2',
199 'AZ': 'wibble',
200 'image': self.conf.image_ref,
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000201 'flavor': self.conf.instance_type}}
202 self.update_stack(stack_identifier, self.template,
203 environment=env2, files=files)
204
205 # replacement will cause the resource physical_resource_id to change.
206 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
207 self.assertNotEqual(orig_asg_id, rsrc.physical_resource_id)
Angus Salkeldd67cf702014-12-18 10:40:47 +1000208
209 def test_create_instance_error_causes_group_error(self):
210 """If a resource in an instance group fails to be created, the instance
211 group itself will fail and the broken inner resource will remain.
212 """
213 stack_name = self._stack_rand_name()
214 files = {'provider.yaml': self.bad_instance_template}
215 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
216 'parameters': {'size': 2,
217 'image': self.conf.image_ref,
Angus Salkeldd67cf702014-12-18 10:40:47 +1000218 'flavor': self.conf.instance_type}}
219
220 self.client.stacks.create(
221 stack_name=stack_name,
222 template=self.template,
223 files=files,
224 disable_rollback=True,
225 parameters={},
226 environment=env
227 )
228 self.addCleanup(self.client.stacks.delete, stack_name)
229 stack = self.client.stacks.get(stack_name)
230 stack_identifier = '%s/%s' % (stack_name, stack.id)
231 self._wait_for_stack_status(stack_identifier, 'CREATE_FAILED')
232 initial_resources = {
233 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
234 'JobServerGroup': 'OS::Heat::InstanceGroup'}
235 self.assertEqual(initial_resources,
236 self.list_resources(stack_identifier))
237
Angus Salkeld771235a2015-01-20 15:11:42 +1000238 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
239 'JobServerGroup')
Angus Salkeldd67cf702014-12-18 10:40:47 +1000240 self._assert_instance_state(nested_ident, 0, 2)
241
242 def test_update_instance_error_causes_group_error(self):
243 """If a resource in an instance group fails to be created during an
244 update, the instance group itself will fail and the broken inner
245 resource will remain.
246 """
247 files = {'provider.yaml': self.instance_template}
248 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
249 'parameters': {'size': 2,
250 'image': self.conf.image_ref,
Angus Salkeldd67cf702014-12-18 10:40:47 +1000251 'flavor': self.conf.instance_type}}
252
253 stack_identifier = self.stack_create(template=self.template,
254 files=files,
255 environment=env)
256 initial_resources = {
257 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
258 'JobServerGroup': 'OS::Heat::InstanceGroup'}
259 self.assertEqual(initial_resources,
260 self.list_resources(stack_identifier))
261
262 stack = self.client.stacks.get(stack_identifier)
263 self.assert_instance_count(stack, 2)
Angus Salkeld771235a2015-01-20 15:11:42 +1000264 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
265 'JobServerGroup')
Angus Salkeldd67cf702014-12-18 10:40:47 +1000266 self._assert_instance_state(nested_ident, 2, 0)
Angus Salkeld545dfeb2015-02-03 11:27:40 +1000267 initial_list = [res.resource_name
268 for res in self.client.resources.list(nested_ident)]
Angus Salkeldd67cf702014-12-18 10:40:47 +1000269
270 env['parameters']['size'] = 3
271 files2 = {'provider.yaml': self.bad_instance_template}
272 self.client.stacks.update(
273 stack_id=stack_identifier,
274 template=self.template,
275 files=files2,
276 disable_rollback=True,
277 parameters={},
278 environment=env
279 )
280 self._wait_for_stack_status(stack_identifier, 'UPDATE_FAILED')
281
Angus Salkeld771235a2015-01-20 15:11:42 +1000282 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
283 'JobServerGroup')
Angus Salkeld545dfeb2015-02-03 11:27:40 +1000284 # assert that there are 3 bad instances
285 # 2 resources should be in update failed, and one create failed.
286 for res in self.client.resources.list(nested_ident):
287 if res.resource_name in initial_list:
288 self._wait_for_resource_status(nested_ident,
289 res.resource_name,
290 'UPDATE_FAILED')
291 else:
292 self._wait_for_resource_status(nested_ident,
293 res.resource_name,
294 'CREATE_FAILED')
Angus Salkeld771235a2015-01-20 15:11:42 +1000295
296
297class InstanceGroupUpdatePolicyTest(InstanceGroupTest):
298
299 def ig_tmpl_with_updt_policy(self):
300 templ = json.loads(copy.deepcopy(self.template))
301 up = {"RollingUpdate": {
302 "MinInstancesInService": "1",
303 "MaxBatchSize": "2",
304 "PauseTime": "PT1S"}}
305 templ['Resources']['JobServerGroup']['UpdatePolicy'] = up
306 return templ
307
308 def update_instance_group(self, updt_template,
309 num_updates_expected_on_updt,
310 num_creates_expected_on_updt,
311 num_deletes_expected_on_updt,
312 update_replace):
313
314 # setup stack from the initial template
315 files = {'provider.yaml': self.instance_template}
Angus Salkeld45a4e492015-03-05 17:55:36 +1000316 size = 5
Angus Salkeld771235a2015-01-20 15:11:42 +1000317 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
318 'parameters': {'size': size,
319 'image': self.conf.image_ref,
320 'flavor': self.conf.instance_type}}
321 stack_name = self._stack_rand_name()
322 stack_identifier = self.stack_create(
323 stack_name=stack_name,
324 template=self.ig_tmpl_with_updt_policy(),
325 files=files,
326 environment=env)
327 stack = self.client.stacks.get(stack_identifier)
328 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
329 'JobServerGroup')
330
331 # test that physical resource name of launch configuration is used
332 conf_name = self._stack_output(stack, 'JobServerConfigRef')
333 conf_name_pattern = '%s-JobServerConfig-[a-zA-Z0-9]+$' % stack_name
334 self.assertThat(conf_name,
335 matchers.MatchesRegex(conf_name_pattern))
336
337 # test the number of instances created
338 self.assert_instance_count(stack, size)
339 # saves info from initial list of instances for comparison later
340 init_instances = self.client.resources.list(nested_ident)
341 init_names = [inst.resource_name for inst in init_instances]
342
343 # test stack update
344 self.update_stack(stack_identifier, updt_template,
345 environment=env, files=files)
Angus Salkeld771235a2015-01-20 15:11:42 +1000346 updt_stack = self.client.stacks.get(stack_identifier)
347
348 # test that the launch configuration is replaced
349 updt_conf_name = self._stack_output(updt_stack, 'JobServerConfigRef')
350 self.assertThat(updt_conf_name,
351 matchers.MatchesRegex(conf_name_pattern))
352 self.assertNotEqual(conf_name, updt_conf_name)
353
354 # test that the group size are the same
355 updt_instances = self.client.resources.list(nested_ident)
356 updt_names = [inst.resource_name for inst in updt_instances]
357 self.assertEqual(len(init_names), len(updt_names))
358 for res in updt_instances:
359 self.assertEqual('UPDATE_COMPLETE', res.resource_status)
360
361 # test that the appropriate number of instance names are the same
362 matched_names = set(updt_names) & set(init_names)
363 self.assertEqual(num_updates_expected_on_updt, len(matched_names))
364
365 # test that the appropriate number of new instances are created
366 self.assertEqual(num_creates_expected_on_updt,
367 len(set(updt_names) - set(init_names)))
368
369 # test that the appropriate number of instances are deleted
370 self.assertEqual(num_deletes_expected_on_updt,
371 len(set(init_names) - set(updt_names)))
372
373 # test that the older instances are the ones being deleted
374 if num_deletes_expected_on_updt > 0:
375 deletes_expected = init_names[:num_deletes_expected_on_updt]
376 self.assertNotIn(deletes_expected, updt_names)
377
378 def test_instance_group_update_replace(self):
379 """
380 Test simple update replace with no conflict in batch size and
381 minimum instances in service.
382 """
383 updt_template = self.ig_tmpl_with_updt_policy()
384 grp = updt_template['Resources']['JobServerGroup']
385 policy = grp['UpdatePolicy']['RollingUpdate']
386 policy['MinInstancesInService'] = '1'
387 policy['MaxBatchSize'] = '3'
388 config = updt_template['Resources']['JobServerConfig']
389 config['Properties']['ImageId'] = self.conf.minimal_image_ref
390
391 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000392 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000393 num_creates_expected_on_updt=0,
394 num_deletes_expected_on_updt=0,
395 update_replace=True)
396
397 def test_instance_group_update_replace_with_adjusted_capacity(self):
398 """
399 Test update replace with capacity adjustment due to conflict in
400 batch size and minimum instances in service.
401 """
402 updt_template = self.ig_tmpl_with_updt_policy()
403 grp = updt_template['Resources']['JobServerGroup']
404 policy = grp['UpdatePolicy']['RollingUpdate']
Angus Salkeld45a4e492015-03-05 17:55:36 +1000405 policy['MinInstancesInService'] = '4'
Angus Salkeld771235a2015-01-20 15:11:42 +1000406 policy['MaxBatchSize'] = '4'
407 config = updt_template['Resources']['JobServerConfig']
408 config['Properties']['ImageId'] = self.conf.minimal_image_ref
409
410 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000411 num_updates_expected_on_updt=2,
412 num_creates_expected_on_updt=3,
413 num_deletes_expected_on_updt=3,
Angus Salkeld771235a2015-01-20 15:11:42 +1000414 update_replace=True)
415
416 def test_instance_group_update_replace_huge_batch_size(self):
417 """
418 Test update replace with a huge batch size.
419 """
420 updt_template = self.ig_tmpl_with_updt_policy()
421 group = updt_template['Resources']['JobServerGroup']
422 policy = group['UpdatePolicy']['RollingUpdate']
423 policy['MinInstancesInService'] = '0'
424 policy['MaxBatchSize'] = '20'
425 config = updt_template['Resources']['JobServerConfig']
426 config['Properties']['ImageId'] = self.conf.minimal_image_ref
427
428 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000429 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000430 num_creates_expected_on_updt=0,
431 num_deletes_expected_on_updt=0,
432 update_replace=True)
433
434 def test_instance_group_update_replace_huge_min_in_service(self):
435 """
436 Test update replace with a huge number of minimum instances in service.
437 """
438 updt_template = self.ig_tmpl_with_updt_policy()
439 group = updt_template['Resources']['JobServerGroup']
440 policy = group['UpdatePolicy']['RollingUpdate']
441 policy['MinInstancesInService'] = '20'
Angus Salkeld45a4e492015-03-05 17:55:36 +1000442 policy['MaxBatchSize'] = '2'
Angus Salkeld771235a2015-01-20 15:11:42 +1000443 policy['PauseTime'] = 'PT0S'
444 config = updt_template['Resources']['JobServerConfig']
445 config['Properties']['ImageId'] = self.conf.minimal_image_ref
446
447 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000448 num_updates_expected_on_updt=3,
449 num_creates_expected_on_updt=2,
450 num_deletes_expected_on_updt=2,
Angus Salkeld771235a2015-01-20 15:11:42 +1000451 update_replace=True)
452
453 def test_instance_group_update_no_replace(self):
454 """
455 Test simple update only and no replace (i.e. updated instance flavor
456 in Launch Configuration) with no conflict in batch size and
457 minimum instances in service.
458 """
459 updt_template = self.ig_tmpl_with_updt_policy()
460 group = updt_template['Resources']['JobServerGroup']
461 policy = group['UpdatePolicy']['RollingUpdate']
462 policy['MinInstancesInService'] = '1'
463 policy['MaxBatchSize'] = '3'
464 policy['PauseTime'] = 'PT0S'
465 config = updt_template['Resources']['JobServerConfig']
466 config['Properties']['InstanceType'] = 'm1.tiny'
467
468 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000469 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000470 num_creates_expected_on_updt=0,
471 num_deletes_expected_on_updt=0,
472 update_replace=False)
473
474 def test_instance_group_update_no_replace_with_adjusted_capacity(self):
475 """
476 Test update only and no replace (i.e. updated instance flavor in
477 Launch Configuration) with capacity adjustment due to conflict in
478 batch size and minimum instances in service.
479 """
480 updt_template = self.ig_tmpl_with_updt_policy()
481 group = updt_template['Resources']['JobServerGroup']
482 policy = group['UpdatePolicy']['RollingUpdate']
Angus Salkeld45a4e492015-03-05 17:55:36 +1000483 policy['MinInstancesInService'] = '4'
Angus Salkeld771235a2015-01-20 15:11:42 +1000484 policy['MaxBatchSize'] = '4'
485 policy['PauseTime'] = 'PT0S'
486 config = updt_template['Resources']['JobServerConfig']
487 config['Properties']['InstanceType'] = 'm1.tiny'
488
489 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000490 num_updates_expected_on_updt=2,
491 num_creates_expected_on_updt=3,
492 num_deletes_expected_on_updt=3,
Angus Salkeld771235a2015-01-20 15:11:42 +1000493 update_replace=False)