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