blob: 5c88bedf37d85406d87cbaf8dc0d06b24d73c0db [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
Angus Salkeldebf15d72014-12-10 17:03:15 +100018from heat_integrationtests.common import test
19
20
Angus Salkeldebf15d72014-12-10 17:03:15 +100021class 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"},
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()
108 self.client = self.orchestration_client
109 if not self.conf.image_ref:
110 raise self.skipException("No image configured to test")
Angus Salkeld771235a2015-01-20 15:11:42 +1000111 if not self.conf.minimal_image_ref:
112 raise self.skipException("No minimal image configured to test")
Angus Salkeldebf15d72014-12-10 17:03:15 +1000113 if not self.conf.instance_type:
114 raise self.skipException("No flavor configured to test")
115
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000116 def assert_instance_count(self, stack, expected_count):
117 inst_list = self._stack_output(stack, 'InstanceList')
118 self.assertEqual(expected_count, len(inst_list.split(',')))
119
Angus Salkeldd67cf702014-12-18 10:40:47 +1000120 def _assert_instance_state(self, nested_identifier,
121 num_complete, num_failed):
122 for res in self.client.resources.list(nested_identifier):
123 if 'COMPLETE' in res.resource_status:
124 num_complete = num_complete - 1
125 elif 'FAILED' in res.resource_status:
126 num_failed = num_failed - 1
127 self.assertEqual(0, num_failed)
128 self.assertEqual(0, num_complete)
129
Angus Salkeld771235a2015-01-20 15:11:42 +1000130
131class InstanceGroupBasicTest(InstanceGroupTest):
132
Angus Salkeldebf15d72014-12-10 17:03:15 +1000133 def test_basic_create_works(self):
134 """Make sure the working case is good.
135 Note this combines test_override_aws_ec2_instance into this test as
136 well, which is:
137 If AWS::EC2::Instance is overridden, InstanceGroup will automatically
138 use that overridden resource type.
139 """
140
Angus Salkeldebf15d72014-12-10 17:03:15 +1000141 files = {'provider.yaml': self.instance_template}
142 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
143 'parameters': {'size': 4,
144 'image': self.conf.image_ref,
Angus Salkeldebf15d72014-12-10 17:03:15 +1000145 'flavor': self.conf.instance_type}}
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000146 stack_identifier = self.stack_create(template=self.template,
147 files=files, environment=env)
Angus Salkeldebf15d72014-12-10 17:03:15 +1000148 initial_resources = {
149 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
150 'JobServerGroup': 'OS::Heat::InstanceGroup'}
151 self.assertEqual(initial_resources,
152 self.list_resources(stack_identifier))
153
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000154 stack = self.client.stacks.get(stack_identifier)
155 self.assert_instance_count(stack, 4)
156
157 def test_size_updates_work(self):
158 files = {'provider.yaml': self.instance_template}
159 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
160 'parameters': {'size': 2,
161 'image': self.conf.image_ref,
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000162 'flavor': self.conf.instance_type}}
163
164 stack_identifier = self.stack_create(template=self.template,
165 files=files,
166 environment=env)
167 stack = self.client.stacks.get(stack_identifier)
168 self.assert_instance_count(stack, 2)
169
170 # Increase min size to 5
171 env2 = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
172 'parameters': {'size': 5,
173 'image': self.conf.image_ref,
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000174 'flavor': self.conf.instance_type}}
175 self.update_stack(stack_identifier, self.template,
176 environment=env2, files=files)
177 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
178 stack = self.client.stacks.get(stack_identifier)
179 self.assert_instance_count(stack, 5)
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000180
181 def test_update_group_replace(self):
182 """Make sure that during a group update the non updatable
183 properties cause a replacement.
184 """
185 files = {'provider.yaml': self.instance_template}
186 env = {'resource_registry':
187 {'AWS::EC2::Instance': 'provider.yaml'},
188 'parameters': {'size': 1,
189 'image': self.conf.image_ref,
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000190 'flavor': self.conf.instance_type}}
191
192 stack_identifier = self.stack_create(template=self.template,
193 files=files,
194 environment=env)
195 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
196 orig_asg_id = rsrc.physical_resource_id
197
198 env2 = {'resource_registry':
199 {'AWS::EC2::Instance': 'provider.yaml'},
200 'parameters': {'size': '2',
201 'AZ': 'wibble',
202 'image': self.conf.image_ref,
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000203 'flavor': self.conf.instance_type}}
204 self.update_stack(stack_identifier, self.template,
205 environment=env2, files=files)
206
207 # replacement will cause the resource physical_resource_id to change.
208 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
209 self.assertNotEqual(orig_asg_id, rsrc.physical_resource_id)
Angus Salkeldd67cf702014-12-18 10:40:47 +1000210
211 def test_create_instance_error_causes_group_error(self):
212 """If a resource in an instance group fails to be created, the instance
213 group itself will fail and the broken inner resource will remain.
214 """
215 stack_name = self._stack_rand_name()
216 files = {'provider.yaml': self.bad_instance_template}
217 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
218 'parameters': {'size': 2,
219 'image': self.conf.image_ref,
Angus Salkeldd67cf702014-12-18 10:40:47 +1000220 'flavor': self.conf.instance_type}}
221
222 self.client.stacks.create(
223 stack_name=stack_name,
224 template=self.template,
225 files=files,
226 disable_rollback=True,
227 parameters={},
228 environment=env
229 )
230 self.addCleanup(self.client.stacks.delete, stack_name)
231 stack = self.client.stacks.get(stack_name)
232 stack_identifier = '%s/%s' % (stack_name, stack.id)
233 self._wait_for_stack_status(stack_identifier, 'CREATE_FAILED')
234 initial_resources = {
235 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
236 'JobServerGroup': 'OS::Heat::InstanceGroup'}
237 self.assertEqual(initial_resources,
238 self.list_resources(stack_identifier))
239
Angus Salkeld771235a2015-01-20 15:11:42 +1000240 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
241 'JobServerGroup')
Angus Salkeldd67cf702014-12-18 10:40:47 +1000242 self._assert_instance_state(nested_ident, 0, 2)
243
244 def test_update_instance_error_causes_group_error(self):
245 """If a resource in an instance group fails to be created during an
246 update, the instance group itself will fail and the broken inner
247 resource will remain.
248 """
249 files = {'provider.yaml': self.instance_template}
250 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
251 'parameters': {'size': 2,
252 'image': self.conf.image_ref,
Angus Salkeldd67cf702014-12-18 10:40:47 +1000253 'flavor': self.conf.instance_type}}
254
255 stack_identifier = self.stack_create(template=self.template,
256 files=files,
257 environment=env)
258 initial_resources = {
259 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
260 'JobServerGroup': 'OS::Heat::InstanceGroup'}
261 self.assertEqual(initial_resources,
262 self.list_resources(stack_identifier))
263
264 stack = self.client.stacks.get(stack_identifier)
265 self.assert_instance_count(stack, 2)
Angus Salkeld771235a2015-01-20 15:11:42 +1000266 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
267 'JobServerGroup')
Angus Salkeldd67cf702014-12-18 10:40:47 +1000268 self._assert_instance_state(nested_ident, 2, 0)
Angus Salkeld545dfeb2015-02-03 11:27:40 +1000269 initial_list = [res.resource_name
270 for res in self.client.resources.list(nested_ident)]
Angus Salkeldd67cf702014-12-18 10:40:47 +1000271
272 env['parameters']['size'] = 3
273 files2 = {'provider.yaml': self.bad_instance_template}
274 self.client.stacks.update(
275 stack_id=stack_identifier,
276 template=self.template,
277 files=files2,
278 disable_rollback=True,
279 parameters={},
280 environment=env
281 )
282 self._wait_for_stack_status(stack_identifier, 'UPDATE_FAILED')
283
Angus Salkeld771235a2015-01-20 15:11:42 +1000284 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
285 'JobServerGroup')
Angus Salkeld545dfeb2015-02-03 11:27:40 +1000286 # assert that there are 3 bad instances
287 # 2 resources should be in update failed, and one create failed.
288 for res in self.client.resources.list(nested_ident):
289 if res.resource_name in initial_list:
290 self._wait_for_resource_status(nested_ident,
291 res.resource_name,
292 'UPDATE_FAILED')
293 else:
294 self._wait_for_resource_status(nested_ident,
295 res.resource_name,
296 'CREATE_FAILED')
Angus Salkeld771235a2015-01-20 15:11:42 +1000297
298
299class InstanceGroupUpdatePolicyTest(InstanceGroupTest):
300
301 def ig_tmpl_with_updt_policy(self):
302 templ = json.loads(copy.deepcopy(self.template))
303 up = {"RollingUpdate": {
304 "MinInstancesInService": "1",
305 "MaxBatchSize": "2",
306 "PauseTime": "PT1S"}}
307 templ['Resources']['JobServerGroup']['UpdatePolicy'] = up
308 return templ
309
310 def update_instance_group(self, updt_template,
311 num_updates_expected_on_updt,
312 num_creates_expected_on_updt,
313 num_deletes_expected_on_updt,
314 update_replace):
315
316 # setup stack from the initial template
317 files = {'provider.yaml': self.instance_template}
Angus Salkeld45a4e492015-03-05 17:55:36 +1000318 size = 5
Angus Salkeld771235a2015-01-20 15:11:42 +1000319 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
320 'parameters': {'size': size,
321 'image': self.conf.image_ref,
322 'flavor': self.conf.instance_type}}
323 stack_name = self._stack_rand_name()
324 stack_identifier = self.stack_create(
325 stack_name=stack_name,
326 template=self.ig_tmpl_with_updt_policy(),
327 files=files,
328 environment=env)
329 stack = self.client.stacks.get(stack_identifier)
330 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
331 'JobServerGroup')
332
333 # test that physical resource name of launch configuration is used
334 conf_name = self._stack_output(stack, 'JobServerConfigRef')
335 conf_name_pattern = '%s-JobServerConfig-[a-zA-Z0-9]+$' % stack_name
336 self.assertThat(conf_name,
337 matchers.MatchesRegex(conf_name_pattern))
338
339 # test the number of instances created
340 self.assert_instance_count(stack, size)
341 # saves info from initial list of instances for comparison later
342 init_instances = self.client.resources.list(nested_ident)
343 init_names = [inst.resource_name for inst in init_instances]
344
345 # test stack update
346 self.update_stack(stack_identifier, updt_template,
347 environment=env, files=files)
348 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
349 updt_stack = self.client.stacks.get(stack_identifier)
350
351 # test that the launch configuration is replaced
352 updt_conf_name = self._stack_output(updt_stack, 'JobServerConfigRef')
353 self.assertThat(updt_conf_name,
354 matchers.MatchesRegex(conf_name_pattern))
355 self.assertNotEqual(conf_name, updt_conf_name)
356
357 # test that the group size are the same
358 updt_instances = self.client.resources.list(nested_ident)
359 updt_names = [inst.resource_name for inst in updt_instances]
360 self.assertEqual(len(init_names), len(updt_names))
361 for res in updt_instances:
362 self.assertEqual('UPDATE_COMPLETE', res.resource_status)
363
364 # test that the appropriate number of instance names are the same
365 matched_names = set(updt_names) & set(init_names)
366 self.assertEqual(num_updates_expected_on_updt, len(matched_names))
367
368 # test that the appropriate number of new instances are created
369 self.assertEqual(num_creates_expected_on_updt,
370 len(set(updt_names) - set(init_names)))
371
372 # test that the appropriate number of instances are deleted
373 self.assertEqual(num_deletes_expected_on_updt,
374 len(set(init_names) - set(updt_names)))
375
376 # test that the older instances are the ones being deleted
377 if num_deletes_expected_on_updt > 0:
378 deletes_expected = init_names[:num_deletes_expected_on_updt]
379 self.assertNotIn(deletes_expected, updt_names)
380
381 def test_instance_group_update_replace(self):
382 """
383 Test simple update replace with no conflict in batch size and
384 minimum instances in service.
385 """
386 updt_template = self.ig_tmpl_with_updt_policy()
387 grp = updt_template['Resources']['JobServerGroup']
388 policy = grp['UpdatePolicy']['RollingUpdate']
389 policy['MinInstancesInService'] = '1'
390 policy['MaxBatchSize'] = '3'
391 config = updt_template['Resources']['JobServerConfig']
392 config['Properties']['ImageId'] = self.conf.minimal_image_ref
393
394 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000395 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000396 num_creates_expected_on_updt=0,
397 num_deletes_expected_on_updt=0,
398 update_replace=True)
399
400 def test_instance_group_update_replace_with_adjusted_capacity(self):
401 """
402 Test update replace with capacity adjustment due to conflict in
403 batch size and minimum instances in service.
404 """
405 updt_template = self.ig_tmpl_with_updt_policy()
406 grp = updt_template['Resources']['JobServerGroup']
407 policy = grp['UpdatePolicy']['RollingUpdate']
Angus Salkeld45a4e492015-03-05 17:55:36 +1000408 policy['MinInstancesInService'] = '4'
Angus Salkeld771235a2015-01-20 15:11:42 +1000409 policy['MaxBatchSize'] = '4'
410 config = updt_template['Resources']['JobServerConfig']
411 config['Properties']['ImageId'] = self.conf.minimal_image_ref
412
413 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000414 num_updates_expected_on_updt=2,
415 num_creates_expected_on_updt=3,
416 num_deletes_expected_on_updt=3,
Angus Salkeld771235a2015-01-20 15:11:42 +1000417 update_replace=True)
418
419 def test_instance_group_update_replace_huge_batch_size(self):
420 """
421 Test update replace with a huge batch size.
422 """
423 updt_template = self.ig_tmpl_with_updt_policy()
424 group = updt_template['Resources']['JobServerGroup']
425 policy = group['UpdatePolicy']['RollingUpdate']
426 policy['MinInstancesInService'] = '0'
427 policy['MaxBatchSize'] = '20'
428 config = updt_template['Resources']['JobServerConfig']
429 config['Properties']['ImageId'] = self.conf.minimal_image_ref
430
431 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000432 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000433 num_creates_expected_on_updt=0,
434 num_deletes_expected_on_updt=0,
435 update_replace=True)
436
437 def test_instance_group_update_replace_huge_min_in_service(self):
438 """
439 Test update replace with a huge number of minimum instances in service.
440 """
441 updt_template = self.ig_tmpl_with_updt_policy()
442 group = updt_template['Resources']['JobServerGroup']
443 policy = group['UpdatePolicy']['RollingUpdate']
444 policy['MinInstancesInService'] = '20'
Angus Salkeld45a4e492015-03-05 17:55:36 +1000445 policy['MaxBatchSize'] = '2'
Angus Salkeld771235a2015-01-20 15:11:42 +1000446 policy['PauseTime'] = 'PT0S'
447 config = updt_template['Resources']['JobServerConfig']
448 config['Properties']['ImageId'] = self.conf.minimal_image_ref
449
450 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000451 num_updates_expected_on_updt=3,
452 num_creates_expected_on_updt=2,
453 num_deletes_expected_on_updt=2,
Angus Salkeld771235a2015-01-20 15:11:42 +1000454 update_replace=True)
455
456 def test_instance_group_update_no_replace(self):
457 """
458 Test simple update only and no replace (i.e. updated instance flavor
459 in Launch Configuration) with no conflict in batch size and
460 minimum instances in service.
461 """
462 updt_template = self.ig_tmpl_with_updt_policy()
463 group = updt_template['Resources']['JobServerGroup']
464 policy = group['UpdatePolicy']['RollingUpdate']
465 policy['MinInstancesInService'] = '1'
466 policy['MaxBatchSize'] = '3'
467 policy['PauseTime'] = 'PT0S'
468 config = updt_template['Resources']['JobServerConfig']
469 config['Properties']['InstanceType'] = 'm1.tiny'
470
471 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000472 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000473 num_creates_expected_on_updt=0,
474 num_deletes_expected_on_updt=0,
475 update_replace=False)
476
477 def test_instance_group_update_no_replace_with_adjusted_capacity(self):
478 """
479 Test update only and no replace (i.e. updated instance flavor in
480 Launch Configuration) with capacity adjustment due to conflict in
481 batch size and minimum instances in service.
482 """
483 updt_template = self.ig_tmpl_with_updt_policy()
484 group = updt_template['Resources']['JobServerGroup']
485 policy = group['UpdatePolicy']['RollingUpdate']
Angus Salkeld45a4e492015-03-05 17:55:36 +1000486 policy['MinInstancesInService'] = '4'
Angus Salkeld771235a2015-01-20 15:11:42 +1000487 policy['MaxBatchSize'] = '4'
488 policy['PauseTime'] = 'PT0S'
489 config = updt_template['Resources']['JobServerConfig']
490 config['Properties']['InstanceType'] = 'm1.tiny'
491
492 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000493 num_updates_expected_on_updt=2,
494 num_creates_expected_on_updt=3,
495 num_deletes_expected_on_updt=3,
Angus Salkeld771235a2015-01-20 15:11:42 +1000496 update_replace=False)