blob: a8a8fc469d4ad414f6a6cc7ad0928e922738cbe0 [file] [log] [blame]
Angus Salkeld28339012015-01-20 19:15:37 +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 copy
14import json
15import logging
16
17from testtools import matchers
18
19from heat_integrationtests.common import test
20
21
22LOG = logging.getLogger(__name__)
23
24
25class AutoscalingGroupTest(test.HeatIntegrationTest):
26
27 template = '''
28{
29 "AWSTemplateFormatVersion" : "2010-09-09",
30 "Description" : "Template to create multiple instances.",
31 "Parameters" : {"size": {"Type": "String", "Default": "1"},
32 "AZ": {"Type": "String", "Default": "nova"},
33 "image": {"Type": "String"},
34 "flavor": {"Type": "String"}},
35 "Resources": {
36 "JobServerGroup": {
37 "Type" : "AWS::AutoScaling::AutoScalingGroup",
38 "Properties" : {
39 "AvailabilityZones" : [{"Ref": "AZ"}],
40 "LaunchConfigurationName" : { "Ref" : "JobServerConfig" },
41 "MinSize" : {"Ref": "size"},
42 "MaxSize" : "20"
43 }
44 },
45
46 "JobServerConfig" : {
47 "Type" : "AWS::AutoScaling::LaunchConfiguration",
48 "Metadata": {"foo": "bar"},
49 "Properties": {
50 "ImageId" : {"Ref": "image"},
51 "InstanceType" : {"Ref": "flavor"},
52 "SecurityGroups" : [ "sg-1" ],
53 "UserData" : "jsconfig data"
54 }
55 }
56 },
57 "Outputs": {
58 "InstanceList": {"Value": {
59 "Fn::GetAtt": ["JobServerGroup", "InstanceList"]}},
60 "JobServerConfigRef": {"Value": {
61 "Ref": "JobServerConfig"}}
62 }
63}
64'''
65
66 instance_template = '''
67heat_template_version: 2013-05-23
68parameters:
69 ImageId: {type: string}
70 InstanceType: {type: string}
71 SecurityGroups: {type: comma_delimited_list}
72 UserData: {type: string}
73 Tags: {type: comma_delimited_list}
74
75resources:
76 random1:
77 type: OS::Heat::RandomString
78 properties:
79 salt: {get_param: ImageId}
80outputs:
81 PublicIp:
82 value: {get_attr: [random1, value]}
83'''
84
85 # This is designed to fail.
86 bad_instance_template = '''
87heat_template_version: 2013-05-23
88parameters:
89 ImageId: {type: string}
90 InstanceType: {type: string}
91 SecurityGroups: {type: comma_delimited_list}
92 UserData: {type: string}
93 Tags: {type: comma_delimited_list}
94
95resources:
96 random1:
97 type: OS::Heat::RandomString
98 depends_on: waiter
99 ready_poster:
100 type: AWS::CloudFormation::WaitConditionHandle
101 waiter:
102 type: AWS::CloudFormation::WaitCondition
103 properties:
104 Handle: {Ref: ready_poster}
105 Timeout: 1
106outputs:
107 PublicIp:
108 value: {get_attr: [random1, value]}
109'''
110
111 def setUp(self):
112 super(AutoscalingGroupTest, self).setUp()
113 self.client = self.orchestration_client
114 if not self.conf.image_ref:
115 raise self.skipException("No image configured to test")
116 if not self.conf.minimal_image_ref:
117 raise self.skipException("No minimal image configured to test")
118 if not self.conf.instance_type:
119 raise self.skipException("No flavor configured to test")
120
121 def assert_instance_count(self, stack, expected_count):
122 inst_list = self._stack_output(stack, 'InstanceList')
123 self.assertEqual(expected_count, len(inst_list.split(',')))
124
125 def _assert_instance_state(self, nested_identifier,
126 num_complete, num_failed):
127 for res in self.client.resources.list(nested_identifier):
128 if 'COMPLETE' in res.resource_status:
129 num_complete = num_complete - 1
130 elif 'FAILED' in res.resource_status:
131 num_failed = num_failed - 1
132 self.assertEqual(0, num_failed)
133 self.assertEqual(0, num_complete)
134
135
136class AutoscalingGroupBasicTest(AutoscalingGroupTest):
137
138 def test_basic_create_works(self):
139 """Make sure the working case is good.
140
141 Note this combines test_override_aws_ec2_instance into this test as
142 well, which is:
143 If AWS::EC2::Instance is overridden, AutoScalingGroup will
144 automatically use that overridden resource type.
145 """
146
147 files = {'provider.yaml': self.instance_template}
148 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
149 'parameters': {'size': 4,
150 'image': self.conf.image_ref,
151 'flavor': self.conf.instance_type}}
152 stack_identifier = self.stack_create(template=self.template,
153 files=files, environment=env)
154 initial_resources = {
155 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
156 'JobServerGroup': 'AWS::AutoScaling::AutoScalingGroup'}
157 self.assertEqual(initial_resources,
158 self.list_resources(stack_identifier))
159
160 stack = self.client.stacks.get(stack_identifier)
161 self.assert_instance_count(stack, 4)
162
163 def test_size_updates_work(self):
164 files = {'provider.yaml': self.instance_template}
165 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
166 'parameters': {'size': 2,
167 'image': self.conf.image_ref,
168 'flavor': self.conf.instance_type}}
169
170 stack_identifier = self.stack_create(template=self.template,
171 files=files,
172 environment=env)
173 stack = self.client.stacks.get(stack_identifier)
174 self.assert_instance_count(stack, 2)
175
176 # Increase min size to 5
177 env2 = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
178 'parameters': {'size': 5,
179 'image': self.conf.image_ref,
180 'flavor': self.conf.instance_type}}
181 self.update_stack(stack_identifier, self.template,
182 environment=env2, files=files)
183 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
184 stack = self.client.stacks.get(stack_identifier)
185 self.assert_instance_count(stack, 5)
186
187 def test_update_group_replace(self):
188 """Make sure that during a group update the non updatable
189 properties cause a replacement.
190 """
191 files = {'provider.yaml': self.instance_template}
192 env = {'resource_registry':
193 {'AWS::EC2::Instance': 'provider.yaml'},
194 'parameters': {'size': '1',
195 'image': self.conf.image_ref,
196 'flavor': self.conf.instance_type}}
197
198 stack_identifier = self.stack_create(template=self.template,
199 files=files,
200 environment=env)
201 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
202 orig_asg_id = rsrc.physical_resource_id
203
204 env2 = {'resource_registry':
205 {'AWS::EC2::Instance': 'provider.yaml'},
206 'parameters': {'size': '1',
207 'AZ': 'wibble',
208 'image': self.conf.image_ref,
209 'flavor': self.conf.instance_type}}
210 self.update_stack(stack_identifier, self.template,
211 environment=env2, files=files)
212
213 # replacement will cause the resource physical_resource_id to change.
214 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
215 self.assertNotEqual(orig_asg_id, rsrc.physical_resource_id)
216
217 def test_create_instance_error_causes_group_error(self):
218 """If a resource in an instance group fails to be created, the instance
219 group itself will fail and the broken inner resource will remain.
220 """
221 stack_name = self._stack_rand_name()
222 files = {'provider.yaml': self.bad_instance_template}
223 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
224 'parameters': {'size': 2,
225 'image': self.conf.image_ref,
226 'flavor': self.conf.instance_type}}
227
228 self.client.stacks.create(
229 stack_name=stack_name,
230 template=self.template,
231 files=files,
232 disable_rollback=True,
233 parameters={},
234 environment=env
235 )
236 self.addCleanup(self.client.stacks.delete, stack_name)
237 stack = self.client.stacks.get(stack_name)
238 stack_identifier = '%s/%s' % (stack_name, stack.id)
239 self._wait_for_stack_status(stack_identifier, 'CREATE_FAILED')
240 initial_resources = {
241 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
242 'JobServerGroup': 'AWS::AutoScaling::AutoScalingGroup'}
243 self.assertEqual(initial_resources,
244 self.list_resources(stack_identifier))
245
246 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
247 'JobServerGroup')
248 self._assert_instance_state(nested_ident, 0, 2)
249
250 def test_update_instance_error_causes_group_error(self):
251 """If a resource in an instance group fails to be created during an
252 update, the instance group itself will fail and the broken inner
253 resource will remain.
254 """
255 files = {'provider.yaml': self.instance_template}
256 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
257 'parameters': {'size': 2,
258 'image': self.conf.image_ref,
259 'flavor': self.conf.instance_type}}
260
261 stack_identifier = self.stack_create(template=self.template,
262 files=files,
263 environment=env)
264 initial_resources = {
265 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
266 'JobServerGroup': 'AWS::AutoScaling::AutoScalingGroup'}
267 self.assertEqual(initial_resources,
268 self.list_resources(stack_identifier))
269
270 stack = self.client.stacks.get(stack_identifier)
271 self.assert_instance_count(stack, 2)
272 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
273 'JobServerGroup')
274 self._assert_instance_state(nested_ident, 2, 0)
Angus Salkeldd4b6bc02015-02-04 16:48:45 +1000275 initial_list = [res.resource_name
276 for res in self.client.resources.list(nested_ident)]
Angus Salkeld28339012015-01-20 19:15:37 +1000277
278 env['parameters']['size'] = 3
279 files2 = {'provider.yaml': self.bad_instance_template}
280 self.client.stacks.update(
281 stack_id=stack_identifier,
282 template=self.template,
283 files=files2,
284 disable_rollback=True,
285 parameters={},
286 environment=env
287 )
288 self._wait_for_stack_status(stack_identifier, 'UPDATE_FAILED')
289
290 # assert that there are 3 bad instances
291 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
292 'JobServerGroup')
Angus Salkeldd4b6bc02015-02-04 16:48:45 +1000293
294 # 2 resources should be in update failed, and one create failed.
295 for res in self.client.resources.list(nested_ident):
296 if res.resource_name in initial_list:
297 self._wait_for_resource_status(nested_ident,
298 res.resource_name,
299 'UPDATE_FAILED')
300 else:
301 self._wait_for_resource_status(nested_ident,
302 res.resource_name,
303 'CREATE_FAILED')
Angus Salkeld28339012015-01-20 19:15:37 +1000304
Angus Salkeldf1b10dd2015-02-04 10:57:38 +1000305 def test_group_suspend_resume(self):
306
307 files = {'provider.yaml': self.instance_template}
308 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
309 'parameters': {'size': 4,
310 'image': self.conf.image_ref,
311 'flavor': self.conf.instance_type}}
312 stack_identifier = self.stack_create(template=self.template,
313 files=files, environment=env)
314
315 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
316 'JobServerGroup')
317
318 self.client.actions.suspend(stack_id=stack_identifier)
319 self._wait_for_resource_status(
320 stack_identifier, 'JobServerGroup', 'SUSPEND_COMPLETE')
321 for res in self.client.resources.list(nested_ident):
322 self._wait_for_resource_status(nested_ident,
323 res.resource_name,
324 'SUSPEND_COMPLETE')
325
326 self.client.actions.resume(stack_id=stack_identifier)
327 self._wait_for_resource_status(
328 stack_identifier, 'JobServerGroup', 'RESUME_COMPLETE')
329 for res in self.client.resources.list(nested_ident):
330 self._wait_for_resource_status(nested_ident,
331 res.resource_name,
332 'RESUME_COMPLETE')
333
Angus Salkeld28339012015-01-20 19:15:37 +1000334
335class AutoscalingGroupUpdatePolicyTest(AutoscalingGroupTest):
336
337 def ig_tmpl_with_updt_policy(self):
338 templ = json.loads(copy.deepcopy(self.template))
339 up = {"AutoScalingRollingUpdate": {
340 "MinInstancesInService": "1",
341 "MaxBatchSize": "2",
342 "PauseTime": "PT1S"}}
343 templ['Resources']['JobServerGroup']['UpdatePolicy'] = up
344 return templ
345
346 def update_instance_group(self, updt_template,
347 num_updates_expected_on_updt,
348 num_creates_expected_on_updt,
349 num_deletes_expected_on_updt,
350 update_replace):
351
352 # setup stack from the initial template
353 files = {'provider.yaml': self.instance_template}
354 size = 10
355 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
356 'parameters': {'size': size,
357 'image': self.conf.image_ref,
358 'flavor': self.conf.instance_type}}
359 stack_name = self._stack_rand_name()
360 stack_identifier = self.stack_create(
361 stack_name=stack_name,
362 template=self.ig_tmpl_with_updt_policy(),
363 files=files,
364 environment=env)
365 stack = self.client.stacks.get(stack_identifier)
366 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
367 'JobServerGroup')
368
369 # test that physical resource name of launch configuration is used
370 conf_name = self._stack_output(stack, 'JobServerConfigRef')
371 conf_name_pattern = '%s-JobServerConfig-[a-zA-Z0-9]+$' % stack_name
372 self.assertThat(conf_name,
373 matchers.MatchesRegex(conf_name_pattern))
374
375 # test the number of instances created
376 self.assert_instance_count(stack, size)
377 # saves info from initial list of instances for comparison later
378 init_instances = self.client.resources.list(nested_ident)
379 init_names = [inst.resource_name for inst in init_instances]
380
381 # test stack update
382 self.update_stack(stack_identifier, updt_template,
383 environment=env, files=files)
384 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
385 updt_stack = self.client.stacks.get(stack_identifier)
386
387 # test that the launch configuration is replaced
388 updt_conf_name = self._stack_output(updt_stack, 'JobServerConfigRef')
389 self.assertThat(updt_conf_name,
390 matchers.MatchesRegex(conf_name_pattern))
391 self.assertNotEqual(conf_name, updt_conf_name)
392
393 # test that the group size are the same
394 updt_instances = self.client.resources.list(nested_ident)
395 updt_names = [inst.resource_name for inst in updt_instances]
396 self.assertEqual(len(init_names), len(updt_names))
397 for res in updt_instances:
398 self.assertEqual('UPDATE_COMPLETE', res.resource_status)
399
400 # test that the appropriate number of instance names are the same
401 matched_names = set(updt_names) & set(init_names)
402 self.assertEqual(num_updates_expected_on_updt, len(matched_names))
403
404 # test that the appropriate number of new instances are created
405 self.assertEqual(num_creates_expected_on_updt,
406 len(set(updt_names) - set(init_names)))
407
408 # test that the appropriate number of instances are deleted
409 self.assertEqual(num_deletes_expected_on_updt,
410 len(set(init_names) - set(updt_names)))
411
412 # test that the older instances are the ones being deleted
413 if num_deletes_expected_on_updt > 0:
414 deletes_expected = init_names[:num_deletes_expected_on_updt]
415 self.assertNotIn(deletes_expected, updt_names)
416
417 def test_instance_group_update_replace(self):
418 """
419 Test simple update replace with no conflict in batch size and
420 minimum instances in service.
421 """
422 updt_template = self.ig_tmpl_with_updt_policy()
423 grp = updt_template['Resources']['JobServerGroup']
424 policy = grp['UpdatePolicy']['AutoScalingRollingUpdate']
425 policy['MinInstancesInService'] = '1'
426 policy['MaxBatchSize'] = '3'
427 config = updt_template['Resources']['JobServerConfig']
428 config['Properties']['ImageId'] = self.conf.minimal_image_ref
429
430 self.update_instance_group(updt_template,
431 num_updates_expected_on_updt=10,
432 num_creates_expected_on_updt=0,
433 num_deletes_expected_on_updt=0,
434 update_replace=True)
435
436 def test_instance_group_update_replace_with_adjusted_capacity(self):
437 """
438 Test update replace with capacity adjustment due to conflict in
439 batch size and minimum instances in service.
440 """
441 updt_template = self.ig_tmpl_with_updt_policy()
442 grp = updt_template['Resources']['JobServerGroup']
443 policy = grp['UpdatePolicy']['AutoScalingRollingUpdate']
444 policy['MinInstancesInService'] = '8'
445 policy['MaxBatchSize'] = '4'
446 config = updt_template['Resources']['JobServerConfig']
447 config['Properties']['ImageId'] = self.conf.minimal_image_ref
448
449 self.update_instance_group(updt_template,
450 num_updates_expected_on_updt=8,
451 num_creates_expected_on_updt=2,
452 num_deletes_expected_on_updt=2,
453 update_replace=True)
454
455 def test_instance_group_update_replace_huge_batch_size(self):
456 """
457 Test update replace with a huge batch size.
458 """
459 updt_template = self.ig_tmpl_with_updt_policy()
460 group = updt_template['Resources']['JobServerGroup']
461 policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
462 policy['MinInstancesInService'] = '0'
463 policy['MaxBatchSize'] = '20'
464 config = updt_template['Resources']['JobServerConfig']
465 config['Properties']['ImageId'] = self.conf.minimal_image_ref
466
467 self.update_instance_group(updt_template,
468 num_updates_expected_on_updt=10,
469 num_creates_expected_on_updt=0,
470 num_deletes_expected_on_updt=0,
471 update_replace=True)
472
473 def test_instance_group_update_replace_huge_min_in_service(self):
474 """
475 Test update replace with a huge number of minimum instances in service.
476 """
477 updt_template = self.ig_tmpl_with_updt_policy()
478 group = updt_template['Resources']['JobServerGroup']
479 policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
480 policy['MinInstancesInService'] = '20'
481 policy['MaxBatchSize'] = '1'
482 policy['PauseTime'] = 'PT0S'
483 config = updt_template['Resources']['JobServerConfig']
484 config['Properties']['ImageId'] = self.conf.minimal_image_ref
485
486 self.update_instance_group(updt_template,
487 num_updates_expected_on_updt=9,
488 num_creates_expected_on_updt=1,
489 num_deletes_expected_on_updt=1,
490 update_replace=True)
491
492 def test_instance_group_update_no_replace(self):
493 """
494 Test simple update only and no replace (i.e. updated instance flavor
495 in Launch Configuration) with no conflict in batch size and
496 minimum instances in service.
497 """
498 updt_template = self.ig_tmpl_with_updt_policy()
499 group = updt_template['Resources']['JobServerGroup']
500 policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
501 policy['MinInstancesInService'] = '1'
502 policy['MaxBatchSize'] = '3'
503 policy['PauseTime'] = 'PT0S'
504 config = updt_template['Resources']['JobServerConfig']
505 config['Properties']['InstanceType'] = 'm1.tiny'
506
507 self.update_instance_group(updt_template,
508 num_updates_expected_on_updt=10,
509 num_creates_expected_on_updt=0,
510 num_deletes_expected_on_updt=0,
511 update_replace=False)
512
513 def test_instance_group_update_no_replace_with_adjusted_capacity(self):
514 """
515 Test update only and no replace (i.e. updated instance flavor in
516 Launch Configuration) with capacity adjustment due to conflict in
517 batch size and minimum instances in service.
518 """
519 updt_template = self.ig_tmpl_with_updt_policy()
520 group = updt_template['Resources']['JobServerGroup']
521 policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
522 policy['MinInstancesInService'] = '8'
523 policy['MaxBatchSize'] = '4'
524 policy['PauseTime'] = 'PT0S'
525 config = updt_template['Resources']['JobServerConfig']
526 config['Properties']['InstanceType'] = 'm1.tiny'
527
528 self.update_instance_group(updt_template,
529 num_updates_expected_on_updt=8,
530 num_creates_expected_on_updt=2,
531 num_deletes_expected_on_updt=2,
532 update_replace=False)