blob: a169c710a1bad09129bcae77717d3bacc58ff551 [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
305
306class AutoscalingGroupUpdatePolicyTest(AutoscalingGroupTest):
307
308 def ig_tmpl_with_updt_policy(self):
309 templ = json.loads(copy.deepcopy(self.template))
310 up = {"AutoScalingRollingUpdate": {
311 "MinInstancesInService": "1",
312 "MaxBatchSize": "2",
313 "PauseTime": "PT1S"}}
314 templ['Resources']['JobServerGroup']['UpdatePolicy'] = up
315 return templ
316
317 def update_instance_group(self, updt_template,
318 num_updates_expected_on_updt,
319 num_creates_expected_on_updt,
320 num_deletes_expected_on_updt,
321 update_replace):
322
323 # setup stack from the initial template
324 files = {'provider.yaml': self.instance_template}
325 size = 10
326 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
327 'parameters': {'size': size,
328 'image': self.conf.image_ref,
329 'flavor': self.conf.instance_type}}
330 stack_name = self._stack_rand_name()
331 stack_identifier = self.stack_create(
332 stack_name=stack_name,
333 template=self.ig_tmpl_with_updt_policy(),
334 files=files,
335 environment=env)
336 stack = self.client.stacks.get(stack_identifier)
337 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
338 'JobServerGroup')
339
340 # test that physical resource name of launch configuration is used
341 conf_name = self._stack_output(stack, 'JobServerConfigRef')
342 conf_name_pattern = '%s-JobServerConfig-[a-zA-Z0-9]+$' % stack_name
343 self.assertThat(conf_name,
344 matchers.MatchesRegex(conf_name_pattern))
345
346 # test the number of instances created
347 self.assert_instance_count(stack, size)
348 # saves info from initial list of instances for comparison later
349 init_instances = self.client.resources.list(nested_ident)
350 init_names = [inst.resource_name for inst in init_instances]
351
352 # test stack update
353 self.update_stack(stack_identifier, updt_template,
354 environment=env, files=files)
355 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
356 updt_stack = self.client.stacks.get(stack_identifier)
357
358 # test that the launch configuration is replaced
359 updt_conf_name = self._stack_output(updt_stack, 'JobServerConfigRef')
360 self.assertThat(updt_conf_name,
361 matchers.MatchesRegex(conf_name_pattern))
362 self.assertNotEqual(conf_name, updt_conf_name)
363
364 # test that the group size are the same
365 updt_instances = self.client.resources.list(nested_ident)
366 updt_names = [inst.resource_name for inst in updt_instances]
367 self.assertEqual(len(init_names), len(updt_names))
368 for res in updt_instances:
369 self.assertEqual('UPDATE_COMPLETE', res.resource_status)
370
371 # test that the appropriate number of instance names are the same
372 matched_names = set(updt_names) & set(init_names)
373 self.assertEqual(num_updates_expected_on_updt, len(matched_names))
374
375 # test that the appropriate number of new instances are created
376 self.assertEqual(num_creates_expected_on_updt,
377 len(set(updt_names) - set(init_names)))
378
379 # test that the appropriate number of instances are deleted
380 self.assertEqual(num_deletes_expected_on_updt,
381 len(set(init_names) - set(updt_names)))
382
383 # test that the older instances are the ones being deleted
384 if num_deletes_expected_on_updt > 0:
385 deletes_expected = init_names[:num_deletes_expected_on_updt]
386 self.assertNotIn(deletes_expected, updt_names)
387
388 def test_instance_group_update_replace(self):
389 """
390 Test simple update replace with no conflict in batch size and
391 minimum instances in service.
392 """
393 updt_template = self.ig_tmpl_with_updt_policy()
394 grp = updt_template['Resources']['JobServerGroup']
395 policy = grp['UpdatePolicy']['AutoScalingRollingUpdate']
396 policy['MinInstancesInService'] = '1'
397 policy['MaxBatchSize'] = '3'
398 config = updt_template['Resources']['JobServerConfig']
399 config['Properties']['ImageId'] = self.conf.minimal_image_ref
400
401 self.update_instance_group(updt_template,
402 num_updates_expected_on_updt=10,
403 num_creates_expected_on_updt=0,
404 num_deletes_expected_on_updt=0,
405 update_replace=True)
406
407 def test_instance_group_update_replace_with_adjusted_capacity(self):
408 """
409 Test update replace with capacity adjustment due to conflict in
410 batch size and minimum instances in service.
411 """
412 updt_template = self.ig_tmpl_with_updt_policy()
413 grp = updt_template['Resources']['JobServerGroup']
414 policy = grp['UpdatePolicy']['AutoScalingRollingUpdate']
415 policy['MinInstancesInService'] = '8'
416 policy['MaxBatchSize'] = '4'
417 config = updt_template['Resources']['JobServerConfig']
418 config['Properties']['ImageId'] = self.conf.minimal_image_ref
419
420 self.update_instance_group(updt_template,
421 num_updates_expected_on_updt=8,
422 num_creates_expected_on_updt=2,
423 num_deletes_expected_on_updt=2,
424 update_replace=True)
425
426 def test_instance_group_update_replace_huge_batch_size(self):
427 """
428 Test update replace with a huge batch size.
429 """
430 updt_template = self.ig_tmpl_with_updt_policy()
431 group = updt_template['Resources']['JobServerGroup']
432 policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
433 policy['MinInstancesInService'] = '0'
434 policy['MaxBatchSize'] = '20'
435 config = updt_template['Resources']['JobServerConfig']
436 config['Properties']['ImageId'] = self.conf.minimal_image_ref
437
438 self.update_instance_group(updt_template,
439 num_updates_expected_on_updt=10,
440 num_creates_expected_on_updt=0,
441 num_deletes_expected_on_updt=0,
442 update_replace=True)
443
444 def test_instance_group_update_replace_huge_min_in_service(self):
445 """
446 Test update replace with a huge number of minimum instances in service.
447 """
448 updt_template = self.ig_tmpl_with_updt_policy()
449 group = updt_template['Resources']['JobServerGroup']
450 policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
451 policy['MinInstancesInService'] = '20'
452 policy['MaxBatchSize'] = '1'
453 policy['PauseTime'] = 'PT0S'
454 config = updt_template['Resources']['JobServerConfig']
455 config['Properties']['ImageId'] = self.conf.minimal_image_ref
456
457 self.update_instance_group(updt_template,
458 num_updates_expected_on_updt=9,
459 num_creates_expected_on_updt=1,
460 num_deletes_expected_on_updt=1,
461 update_replace=True)
462
463 def test_instance_group_update_no_replace(self):
464 """
465 Test simple update only and no replace (i.e. updated instance flavor
466 in Launch Configuration) with no conflict in batch size and
467 minimum instances in service.
468 """
469 updt_template = self.ig_tmpl_with_updt_policy()
470 group = updt_template['Resources']['JobServerGroup']
471 policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
472 policy['MinInstancesInService'] = '1'
473 policy['MaxBatchSize'] = '3'
474 policy['PauseTime'] = 'PT0S'
475 config = updt_template['Resources']['JobServerConfig']
476 config['Properties']['InstanceType'] = 'm1.tiny'
477
478 self.update_instance_group(updt_template,
479 num_updates_expected_on_updt=10,
480 num_creates_expected_on_updt=0,
481 num_deletes_expected_on_updt=0,
482 update_replace=False)
483
484 def test_instance_group_update_no_replace_with_adjusted_capacity(self):
485 """
486 Test update only and no replace (i.e. updated instance flavor in
487 Launch Configuration) with capacity adjustment due to conflict in
488 batch size and minimum instances in service.
489 """
490 updt_template = self.ig_tmpl_with_updt_policy()
491 group = updt_template['Resources']['JobServerGroup']
492 policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
493 policy['MinInstancesInService'] = '8'
494 policy['MaxBatchSize'] = '4'
495 policy['PauseTime'] = 'PT0S'
496 config = updt_template['Resources']['JobServerConfig']
497 config['Properties']['InstanceType'] = 'm1.tiny'
498
499 self.update_instance_group(updt_template,
500 num_updates_expected_on_updt=8,
501 num_creates_expected_on_updt=2,
502 num_deletes_expected_on_updt=2,
503 update_replace=False)