blob: 1455d0b2837631bbf2d4bb1648be3b1771957d18 [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)
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000177 stack = self.client.stacks.get(stack_identifier)
178 self.assert_instance_count(stack, 5)
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000179
180 def test_update_group_replace(self):
181 """Make sure that during a group update the non updatable
182 properties cause a replacement.
183 """
184 files = {'provider.yaml': self.instance_template}
185 env = {'resource_registry':
186 {'AWS::EC2::Instance': 'provider.yaml'},
187 'parameters': {'size': 1,
188 'image': self.conf.image_ref,
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000189 'flavor': self.conf.instance_type}}
190
191 stack_identifier = self.stack_create(template=self.template,
192 files=files,
193 environment=env)
194 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
195 orig_asg_id = rsrc.physical_resource_id
196
197 env2 = {'resource_registry':
198 {'AWS::EC2::Instance': 'provider.yaml'},
199 'parameters': {'size': '2',
200 'AZ': 'wibble',
201 'image': self.conf.image_ref,
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000202 'flavor': self.conf.instance_type}}
203 self.update_stack(stack_identifier, self.template,
204 environment=env2, files=files)
205
206 # replacement will cause the resource physical_resource_id to change.
207 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
208 self.assertNotEqual(orig_asg_id, rsrc.physical_resource_id)
Angus Salkeldd67cf702014-12-18 10:40:47 +1000209
210 def test_create_instance_error_causes_group_error(self):
211 """If a resource in an instance group fails to be created, the instance
212 group itself will fail and the broken inner resource will remain.
213 """
214 stack_name = self._stack_rand_name()
215 files = {'provider.yaml': self.bad_instance_template}
216 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
217 'parameters': {'size': 2,
218 'image': self.conf.image_ref,
Angus Salkeldd67cf702014-12-18 10:40:47 +1000219 'flavor': self.conf.instance_type}}
220
221 self.client.stacks.create(
222 stack_name=stack_name,
223 template=self.template,
224 files=files,
225 disable_rollback=True,
226 parameters={},
227 environment=env
228 )
229 self.addCleanup(self.client.stacks.delete, stack_name)
230 stack = self.client.stacks.get(stack_name)
231 stack_identifier = '%s/%s' % (stack_name, stack.id)
232 self._wait_for_stack_status(stack_identifier, 'CREATE_FAILED')
233 initial_resources = {
234 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
235 'JobServerGroup': 'OS::Heat::InstanceGroup'}
236 self.assertEqual(initial_resources,
237 self.list_resources(stack_identifier))
238
Angus Salkeld771235a2015-01-20 15:11:42 +1000239 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
240 'JobServerGroup')
Angus Salkeldd67cf702014-12-18 10:40:47 +1000241 self._assert_instance_state(nested_ident, 0, 2)
242
243 def test_update_instance_error_causes_group_error(self):
244 """If a resource in an instance group fails to be created during an
245 update, the instance group itself will fail and the broken inner
246 resource will remain.
247 """
248 files = {'provider.yaml': self.instance_template}
249 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
250 'parameters': {'size': 2,
251 'image': self.conf.image_ref,
Angus Salkeldd67cf702014-12-18 10:40:47 +1000252 'flavor': self.conf.instance_type}}
253
254 stack_identifier = self.stack_create(template=self.template,
255 files=files,
256 environment=env)
257 initial_resources = {
258 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
259 'JobServerGroup': 'OS::Heat::InstanceGroup'}
260 self.assertEqual(initial_resources,
261 self.list_resources(stack_identifier))
262
263 stack = self.client.stacks.get(stack_identifier)
264 self.assert_instance_count(stack, 2)
Angus Salkeld771235a2015-01-20 15:11:42 +1000265 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
266 'JobServerGroup')
Angus Salkeldd67cf702014-12-18 10:40:47 +1000267 self._assert_instance_state(nested_ident, 2, 0)
Angus Salkeld545dfeb2015-02-03 11:27:40 +1000268 initial_list = [res.resource_name
269 for res in self.client.resources.list(nested_ident)]
Angus Salkeldd67cf702014-12-18 10:40:47 +1000270
271 env['parameters']['size'] = 3
272 files2 = {'provider.yaml': self.bad_instance_template}
273 self.client.stacks.update(
274 stack_id=stack_identifier,
275 template=self.template,
276 files=files2,
277 disable_rollback=True,
278 parameters={},
279 environment=env
280 )
281 self._wait_for_stack_status(stack_identifier, 'UPDATE_FAILED')
282
Angus Salkeld771235a2015-01-20 15:11:42 +1000283 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
284 'JobServerGroup')
Angus Salkeld545dfeb2015-02-03 11:27:40 +1000285 # assert that there are 3 bad instances
286 # 2 resources should be in update failed, and one create failed.
287 for res in self.client.resources.list(nested_ident):
288 if res.resource_name in initial_list:
289 self._wait_for_resource_status(nested_ident,
290 res.resource_name,
291 'UPDATE_FAILED')
292 else:
293 self._wait_for_resource_status(nested_ident,
294 res.resource_name,
295 'CREATE_FAILED')
Angus Salkeld771235a2015-01-20 15:11:42 +1000296
297
298class InstanceGroupUpdatePolicyTest(InstanceGroupTest):
299
300 def ig_tmpl_with_updt_policy(self):
301 templ = json.loads(copy.deepcopy(self.template))
302 up = {"RollingUpdate": {
303 "MinInstancesInService": "1",
304 "MaxBatchSize": "2",
305 "PauseTime": "PT1S"}}
306 templ['Resources']['JobServerGroup']['UpdatePolicy'] = up
307 return templ
308
309 def update_instance_group(self, updt_template,
310 num_updates_expected_on_updt,
311 num_creates_expected_on_updt,
312 num_deletes_expected_on_updt,
313 update_replace):
314
315 # setup stack from the initial template
316 files = {'provider.yaml': self.instance_template}
Angus Salkeld45a4e492015-03-05 17:55:36 +1000317 size = 5
Angus Salkeld771235a2015-01-20 15:11:42 +1000318 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
319 'parameters': {'size': size,
320 'image': self.conf.image_ref,
321 'flavor': self.conf.instance_type}}
322 stack_name = self._stack_rand_name()
323 stack_identifier = self.stack_create(
324 stack_name=stack_name,
325 template=self.ig_tmpl_with_updt_policy(),
326 files=files,
327 environment=env)
328 stack = self.client.stacks.get(stack_identifier)
329 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
330 'JobServerGroup')
331
332 # test that physical resource name of launch configuration is used
333 conf_name = self._stack_output(stack, 'JobServerConfigRef')
334 conf_name_pattern = '%s-JobServerConfig-[a-zA-Z0-9]+$' % stack_name
335 self.assertThat(conf_name,
336 matchers.MatchesRegex(conf_name_pattern))
337
338 # test the number of instances created
339 self.assert_instance_count(stack, size)
340 # saves info from initial list of instances for comparison later
341 init_instances = self.client.resources.list(nested_ident)
342 init_names = [inst.resource_name for inst in init_instances]
343
344 # test stack update
345 self.update_stack(stack_identifier, updt_template,
346 environment=env, files=files)
Angus Salkeld771235a2015-01-20 15:11:42 +1000347 updt_stack = self.client.stacks.get(stack_identifier)
348
349 # test that the launch configuration is replaced
350 updt_conf_name = self._stack_output(updt_stack, 'JobServerConfigRef')
351 self.assertThat(updt_conf_name,
352 matchers.MatchesRegex(conf_name_pattern))
353 self.assertNotEqual(conf_name, updt_conf_name)
354
355 # test that the group size are the same
356 updt_instances = self.client.resources.list(nested_ident)
357 updt_names = [inst.resource_name for inst in updt_instances]
358 self.assertEqual(len(init_names), len(updt_names))
359 for res in updt_instances:
360 self.assertEqual('UPDATE_COMPLETE', res.resource_status)
361
362 # test that the appropriate number of instance names are the same
363 matched_names = set(updt_names) & set(init_names)
364 self.assertEqual(num_updates_expected_on_updt, len(matched_names))
365
366 # test that the appropriate number of new instances are created
367 self.assertEqual(num_creates_expected_on_updt,
368 len(set(updt_names) - set(init_names)))
369
370 # test that the appropriate number of instances are deleted
371 self.assertEqual(num_deletes_expected_on_updt,
372 len(set(init_names) - set(updt_names)))
373
374 # test that the older instances are the ones being deleted
375 if num_deletes_expected_on_updt > 0:
376 deletes_expected = init_names[:num_deletes_expected_on_updt]
377 self.assertNotIn(deletes_expected, updt_names)
378
379 def test_instance_group_update_replace(self):
380 """
381 Test simple update replace with no conflict in batch size and
382 minimum instances in service.
383 """
384 updt_template = self.ig_tmpl_with_updt_policy()
385 grp = updt_template['Resources']['JobServerGroup']
386 policy = grp['UpdatePolicy']['RollingUpdate']
387 policy['MinInstancesInService'] = '1'
388 policy['MaxBatchSize'] = '3'
389 config = updt_template['Resources']['JobServerConfig']
390 config['Properties']['ImageId'] = self.conf.minimal_image_ref
391
392 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000393 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000394 num_creates_expected_on_updt=0,
395 num_deletes_expected_on_updt=0,
396 update_replace=True)
397
398 def test_instance_group_update_replace_with_adjusted_capacity(self):
399 """
400 Test update replace with capacity adjustment due to conflict in
401 batch size and minimum instances in service.
402 """
403 updt_template = self.ig_tmpl_with_updt_policy()
404 grp = updt_template['Resources']['JobServerGroup']
405 policy = grp['UpdatePolicy']['RollingUpdate']
Angus Salkeld45a4e492015-03-05 17:55:36 +1000406 policy['MinInstancesInService'] = '4'
Angus Salkeld771235a2015-01-20 15:11:42 +1000407 policy['MaxBatchSize'] = '4'
408 config = updt_template['Resources']['JobServerConfig']
409 config['Properties']['ImageId'] = self.conf.minimal_image_ref
410
411 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000412 num_updates_expected_on_updt=2,
413 num_creates_expected_on_updt=3,
414 num_deletes_expected_on_updt=3,
Angus Salkeld771235a2015-01-20 15:11:42 +1000415 update_replace=True)
416
417 def test_instance_group_update_replace_huge_batch_size(self):
418 """
419 Test update replace with a huge batch size.
420 """
421 updt_template = self.ig_tmpl_with_updt_policy()
422 group = updt_template['Resources']['JobServerGroup']
423 policy = group['UpdatePolicy']['RollingUpdate']
424 policy['MinInstancesInService'] = '0'
425 policy['MaxBatchSize'] = '20'
426 config = updt_template['Resources']['JobServerConfig']
427 config['Properties']['ImageId'] = self.conf.minimal_image_ref
428
429 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000430 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000431 num_creates_expected_on_updt=0,
432 num_deletes_expected_on_updt=0,
433 update_replace=True)
434
435 def test_instance_group_update_replace_huge_min_in_service(self):
436 """
437 Test update replace with a huge number of minimum instances in service.
438 """
439 updt_template = self.ig_tmpl_with_updt_policy()
440 group = updt_template['Resources']['JobServerGroup']
441 policy = group['UpdatePolicy']['RollingUpdate']
442 policy['MinInstancesInService'] = '20'
Angus Salkeld45a4e492015-03-05 17:55:36 +1000443 policy['MaxBatchSize'] = '2'
Angus Salkeld771235a2015-01-20 15:11:42 +1000444 policy['PauseTime'] = 'PT0S'
445 config = updt_template['Resources']['JobServerConfig']
446 config['Properties']['ImageId'] = self.conf.minimal_image_ref
447
448 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000449 num_updates_expected_on_updt=3,
450 num_creates_expected_on_updt=2,
451 num_deletes_expected_on_updt=2,
Angus Salkeld771235a2015-01-20 15:11:42 +1000452 update_replace=True)
453
454 def test_instance_group_update_no_replace(self):
455 """
456 Test simple update only and no replace (i.e. updated instance flavor
457 in Launch Configuration) with no conflict in batch size and
458 minimum instances in service.
459 """
460 updt_template = self.ig_tmpl_with_updt_policy()
461 group = updt_template['Resources']['JobServerGroup']
462 policy = group['UpdatePolicy']['RollingUpdate']
463 policy['MinInstancesInService'] = '1'
464 policy['MaxBatchSize'] = '3'
465 policy['PauseTime'] = 'PT0S'
466 config = updt_template['Resources']['JobServerConfig']
467 config['Properties']['InstanceType'] = 'm1.tiny'
468
469 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000470 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000471 num_creates_expected_on_updt=0,
472 num_deletes_expected_on_updt=0,
473 update_replace=False)
474
475 def test_instance_group_update_no_replace_with_adjusted_capacity(self):
476 """
477 Test update only and no replace (i.e. updated instance flavor in
478 Launch Configuration) with capacity adjustment due to conflict in
479 batch size and minimum instances in service.
480 """
481 updt_template = self.ig_tmpl_with_updt_policy()
482 group = updt_template['Resources']['JobServerGroup']
483 policy = group['UpdatePolicy']['RollingUpdate']
Angus Salkeld45a4e492015-03-05 17:55:36 +1000484 policy['MinInstancesInService'] = '4'
Angus Salkeld771235a2015-01-20 15:11:42 +1000485 policy['MaxBatchSize'] = '4'
486 policy['PauseTime'] = 'PT0S'
487 config = updt_template['Resources']['JobServerConfig']
488 config['Properties']['InstanceType'] = 'm1.tiny'
489
490 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000491 num_updates_expected_on_updt=2,
492 num_creates_expected_on_updt=3,
493 num_deletes_expected_on_updt=3,
Angus Salkeld771235a2015-01-20 15:11:42 +1000494 update_replace=False)