blob: 3863df6377061fb1d0ba4c250a1db9e360bb67c4 [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 +100015import logging
16
Angus Salkeld771235a2015-01-20 15:11:42 +100017from testtools import matchers
18
Angus Salkeldebf15d72014-12-10 17:03:15 +100019from heat_integrationtests.common import test
20
21
22LOG = logging.getLogger(__name__)
23
24
25class InstanceGroupTest(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"},
Anastasia Kuznetsova33258742015-01-14 16:13:42 +040034 "flavor": {"Type": "String"}},
Angus Salkeldebf15d72014-12-10 17:03:15 +100035 "Resources": {
36 "JobServerGroup": {
37 "Type": "OS::Heat::InstanceGroup",
38 "Properties": {
39 "LaunchConfigurationName" : {"Ref": "JobServerConfig"},
40 "Size" : {"Ref": "size"},
41 "AvailabilityZones" : [{"Ref": "AZ"}]
42 }
43 },
44
45 "JobServerConfig" : {
46 "Type" : "AWS::AutoScaling::LaunchConfiguration",
47 "Metadata": {"foo": "bar"},
48 "Properties": {
49 "ImageId" : {"Ref": "image"},
50 "InstanceType" : {"Ref": "flavor"},
Angus Salkeldebf15d72014-12-10 17:03:15 +100051 "SecurityGroups" : [ "sg-1" ],
Sergey Kraynev9612adc2014-12-19 08:17:08 -050052 "UserData" : "jsconfig data"
Angus Salkeldebf15d72014-12-10 17:03:15 +100053 }
54 }
55 },
56 "Outputs": {
57 "InstanceList": {"Value": {
Angus Salkeld771235a2015-01-20 15:11:42 +100058 "Fn::GetAtt": ["JobServerGroup", "InstanceList"]}},
59 "JobServerConfigRef": {"Value": {
60 "Ref": "JobServerConfig"}}
Angus Salkeldebf15d72014-12-10 17:03:15 +100061 }
62}
63'''
64
65 instance_template = '''
66heat_template_version: 2013-05-23
67parameters:
68 ImageId: {type: string}
69 InstanceType: {type: string}
Angus Salkeldebf15d72014-12-10 17:03:15 +100070 SecurityGroups: {type: comma_delimited_list}
71 UserData: {type: string}
72 Tags: {type: comma_delimited_list}
73
74resources:
75 random1:
76 type: OS::Heat::RandomString
Angus Salkeld771235a2015-01-20 15:11:42 +100077 properties:
78 salt: {get_param: ImageId}
Angus Salkeldebf15d72014-12-10 17:03:15 +100079outputs:
80 PublicIp:
81 value: {get_attr: [random1, value]}
82'''
83
Angus Salkeldd67cf702014-12-18 10:40:47 +100084 # This is designed to fail.
85 bad_instance_template = '''
86heat_template_version: 2013-05-23
87parameters:
88 ImageId: {type: string}
89 InstanceType: {type: string}
Angus Salkeldd67cf702014-12-18 10:40:47 +100090 SecurityGroups: {type: comma_delimited_list}
91 UserData: {type: string}
92 Tags: {type: comma_delimited_list}
93
94resources:
95 random1:
96 type: OS::Heat::RandomString
97 depends_on: waiter
98 ready_poster:
99 type: AWS::CloudFormation::WaitConditionHandle
100 waiter:
101 type: AWS::CloudFormation::WaitCondition
102 properties:
103 Handle: {Ref: ready_poster}
104 Timeout: 1
105outputs:
106 PublicIp:
107 value: {get_attr: [random1, value]}
108'''
109
Angus Salkeldebf15d72014-12-10 17:03:15 +1000110 def setUp(self):
111 super(InstanceGroupTest, self).setUp()
112 self.client = self.orchestration_client
113 if not self.conf.image_ref:
114 raise self.skipException("No image configured to test")
Angus Salkeld771235a2015-01-20 15:11:42 +1000115 if not self.conf.minimal_image_ref:
116 raise self.skipException("No minimal image configured to test")
Angus Salkeldebf15d72014-12-10 17:03:15 +1000117 if not self.conf.instance_type:
118 raise self.skipException("No flavor configured to test")
119
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000120 def assert_instance_count(self, stack, expected_count):
121 inst_list = self._stack_output(stack, 'InstanceList')
122 self.assertEqual(expected_count, len(inst_list.split(',')))
123
Angus Salkeldd67cf702014-12-18 10:40:47 +1000124 def _assert_instance_state(self, nested_identifier,
125 num_complete, num_failed):
126 for res in self.client.resources.list(nested_identifier):
127 if 'COMPLETE' in res.resource_status:
128 num_complete = num_complete - 1
129 elif 'FAILED' in res.resource_status:
130 num_failed = num_failed - 1
131 self.assertEqual(0, num_failed)
132 self.assertEqual(0, num_complete)
133
Angus Salkeld771235a2015-01-20 15:11:42 +1000134
135class InstanceGroupBasicTest(InstanceGroupTest):
136
Angus Salkeldebf15d72014-12-10 17:03:15 +1000137 def test_basic_create_works(self):
138 """Make sure the working case is good.
139 Note this combines test_override_aws_ec2_instance into this test as
140 well, which is:
141 If AWS::EC2::Instance is overridden, InstanceGroup will automatically
142 use that overridden resource type.
143 """
144
Angus Salkeldebf15d72014-12-10 17:03:15 +1000145 files = {'provider.yaml': self.instance_template}
146 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
147 'parameters': {'size': 4,
148 'image': self.conf.image_ref,
Angus Salkeldebf15d72014-12-10 17:03:15 +1000149 'flavor': self.conf.instance_type}}
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000150 stack_identifier = self.stack_create(template=self.template,
151 files=files, environment=env)
Angus Salkeldebf15d72014-12-10 17:03:15 +1000152 initial_resources = {
153 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
154 'JobServerGroup': 'OS::Heat::InstanceGroup'}
155 self.assertEqual(initial_resources,
156 self.list_resources(stack_identifier))
157
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000158 stack = self.client.stacks.get(stack_identifier)
159 self.assert_instance_count(stack, 4)
160
161 def test_size_updates_work(self):
162 files = {'provider.yaml': self.instance_template}
163 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
164 'parameters': {'size': 2,
165 'image': self.conf.image_ref,
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000166 'flavor': self.conf.instance_type}}
167
168 stack_identifier = self.stack_create(template=self.template,
169 files=files,
170 environment=env)
171 stack = self.client.stacks.get(stack_identifier)
172 self.assert_instance_count(stack, 2)
173
174 # Increase min size to 5
175 env2 = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
176 'parameters': {'size': 5,
177 'image': self.conf.image_ref,
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000178 'flavor': self.conf.instance_type}}
179 self.update_stack(stack_identifier, self.template,
180 environment=env2, files=files)
181 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
182 stack = self.client.stacks.get(stack_identifier)
183 self.assert_instance_count(stack, 5)
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000184
185 def test_update_group_replace(self):
186 """Make sure that during a group update the non updatable
187 properties cause a replacement.
188 """
189 files = {'provider.yaml': self.instance_template}
190 env = {'resource_registry':
191 {'AWS::EC2::Instance': 'provider.yaml'},
192 'parameters': {'size': 1,
193 'image': self.conf.image_ref,
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000194 'flavor': self.conf.instance_type}}
195
196 stack_identifier = self.stack_create(template=self.template,
197 files=files,
198 environment=env)
199 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
200 orig_asg_id = rsrc.physical_resource_id
201
202 env2 = {'resource_registry':
203 {'AWS::EC2::Instance': 'provider.yaml'},
204 'parameters': {'size': '2',
205 'AZ': 'wibble',
206 'image': self.conf.image_ref,
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000207 'flavor': self.conf.instance_type}}
208 self.update_stack(stack_identifier, self.template,
209 environment=env2, files=files)
210
211 # replacement will cause the resource physical_resource_id to change.
212 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
213 self.assertNotEqual(orig_asg_id, rsrc.physical_resource_id)
Angus Salkeldd67cf702014-12-18 10:40:47 +1000214
215 def test_create_instance_error_causes_group_error(self):
216 """If a resource in an instance group fails to be created, the instance
217 group itself will fail and the broken inner resource will remain.
218 """
219 stack_name = self._stack_rand_name()
220 files = {'provider.yaml': self.bad_instance_template}
221 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
222 'parameters': {'size': 2,
223 'image': self.conf.image_ref,
Angus Salkeldd67cf702014-12-18 10:40:47 +1000224 'flavor': self.conf.instance_type}}
225
226 self.client.stacks.create(
227 stack_name=stack_name,
228 template=self.template,
229 files=files,
230 disable_rollback=True,
231 parameters={},
232 environment=env
233 )
234 self.addCleanup(self.client.stacks.delete, stack_name)
235 stack = self.client.stacks.get(stack_name)
236 stack_identifier = '%s/%s' % (stack_name, stack.id)
237 self._wait_for_stack_status(stack_identifier, 'CREATE_FAILED')
238 initial_resources = {
239 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
240 'JobServerGroup': 'OS::Heat::InstanceGroup'}
241 self.assertEqual(initial_resources,
242 self.list_resources(stack_identifier))
243
Angus Salkeld771235a2015-01-20 15:11:42 +1000244 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
245 'JobServerGroup')
Angus Salkeldd67cf702014-12-18 10:40:47 +1000246 self._assert_instance_state(nested_ident, 0, 2)
247
248 def test_update_instance_error_causes_group_error(self):
249 """If a resource in an instance group fails to be created during an
250 update, the instance group itself will fail and the broken inner
251 resource will remain.
252 """
253 files = {'provider.yaml': self.instance_template}
254 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
255 'parameters': {'size': 2,
256 'image': self.conf.image_ref,
Angus Salkeldd67cf702014-12-18 10:40:47 +1000257 'flavor': self.conf.instance_type}}
258
259 stack_identifier = self.stack_create(template=self.template,
260 files=files,
261 environment=env)
262 initial_resources = {
263 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
264 'JobServerGroup': 'OS::Heat::InstanceGroup'}
265 self.assertEqual(initial_resources,
266 self.list_resources(stack_identifier))
267
268 stack = self.client.stacks.get(stack_identifier)
269 self.assert_instance_count(stack, 2)
Angus Salkeld771235a2015-01-20 15:11:42 +1000270 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
271 'JobServerGroup')
Angus Salkeldd67cf702014-12-18 10:40:47 +1000272 self._assert_instance_state(nested_ident, 2, 0)
Angus Salkeld545dfeb2015-02-03 11:27:40 +1000273 initial_list = [res.resource_name
274 for res in self.client.resources.list(nested_ident)]
Angus Salkeldd67cf702014-12-18 10:40:47 +1000275
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
Angus Salkeld771235a2015-01-20 15:11:42 +1000288 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
289 'JobServerGroup')
Angus Salkeld545dfeb2015-02-03 11:27:40 +1000290 # assert that there are 3 bad instances
291 # 2 resources should be in update failed, and one create failed.
292 for res in self.client.resources.list(nested_ident):
293 if res.resource_name in initial_list:
294 self._wait_for_resource_status(nested_ident,
295 res.resource_name,
296 'UPDATE_FAILED')
297 else:
298 self._wait_for_resource_status(nested_ident,
299 res.resource_name,
300 'CREATE_FAILED')
Angus Salkeld771235a2015-01-20 15:11:42 +1000301
302
303class InstanceGroupUpdatePolicyTest(InstanceGroupTest):
304
305 def ig_tmpl_with_updt_policy(self):
306 templ = json.loads(copy.deepcopy(self.template))
307 up = {"RollingUpdate": {
308 "MinInstancesInService": "1",
309 "MaxBatchSize": "2",
310 "PauseTime": "PT1S"}}
311 templ['Resources']['JobServerGroup']['UpdatePolicy'] = up
312 return templ
313
314 def update_instance_group(self, updt_template,
315 num_updates_expected_on_updt,
316 num_creates_expected_on_updt,
317 num_deletes_expected_on_updt,
318 update_replace):
319
320 # setup stack from the initial template
321 files = {'provider.yaml': self.instance_template}
Angus Salkeld45a4e492015-03-05 17:55:36 +1000322 size = 5
Angus Salkeld771235a2015-01-20 15:11:42 +1000323 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
324 'parameters': {'size': size,
325 'image': self.conf.image_ref,
326 'flavor': self.conf.instance_type}}
327 stack_name = self._stack_rand_name()
328 stack_identifier = self.stack_create(
329 stack_name=stack_name,
330 template=self.ig_tmpl_with_updt_policy(),
331 files=files,
332 environment=env)
333 stack = self.client.stacks.get(stack_identifier)
334 nested_ident = self.assert_resource_is_a_stack(stack_identifier,
335 'JobServerGroup')
336
337 # test that physical resource name of launch configuration is used
338 conf_name = self._stack_output(stack, 'JobServerConfigRef')
339 conf_name_pattern = '%s-JobServerConfig-[a-zA-Z0-9]+$' % stack_name
340 self.assertThat(conf_name,
341 matchers.MatchesRegex(conf_name_pattern))
342
343 # test the number of instances created
344 self.assert_instance_count(stack, size)
345 # saves info from initial list of instances for comparison later
346 init_instances = self.client.resources.list(nested_ident)
347 init_names = [inst.resource_name for inst in init_instances]
348
349 # test stack update
350 self.update_stack(stack_identifier, updt_template,
351 environment=env, files=files)
352 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
353 updt_stack = self.client.stacks.get(stack_identifier)
354
355 # test that the launch configuration is replaced
356 updt_conf_name = self._stack_output(updt_stack, 'JobServerConfigRef')
357 self.assertThat(updt_conf_name,
358 matchers.MatchesRegex(conf_name_pattern))
359 self.assertNotEqual(conf_name, updt_conf_name)
360
361 # test that the group size are the same
362 updt_instances = self.client.resources.list(nested_ident)
363 updt_names = [inst.resource_name for inst in updt_instances]
364 self.assertEqual(len(init_names), len(updt_names))
365 for res in updt_instances:
366 self.assertEqual('UPDATE_COMPLETE', res.resource_status)
367
368 # test that the appropriate number of instance names are the same
369 matched_names = set(updt_names) & set(init_names)
370 self.assertEqual(num_updates_expected_on_updt, len(matched_names))
371
372 # test that the appropriate number of new instances are created
373 self.assertEqual(num_creates_expected_on_updt,
374 len(set(updt_names) - set(init_names)))
375
376 # test that the appropriate number of instances are deleted
377 self.assertEqual(num_deletes_expected_on_updt,
378 len(set(init_names) - set(updt_names)))
379
380 # test that the older instances are the ones being deleted
381 if num_deletes_expected_on_updt > 0:
382 deletes_expected = init_names[:num_deletes_expected_on_updt]
383 self.assertNotIn(deletes_expected, updt_names)
384
385 def test_instance_group_update_replace(self):
386 """
387 Test simple update replace with no conflict in batch size and
388 minimum instances in service.
389 """
390 updt_template = self.ig_tmpl_with_updt_policy()
391 grp = updt_template['Resources']['JobServerGroup']
392 policy = grp['UpdatePolicy']['RollingUpdate']
393 policy['MinInstancesInService'] = '1'
394 policy['MaxBatchSize'] = '3'
395 config = updt_template['Resources']['JobServerConfig']
396 config['Properties']['ImageId'] = self.conf.minimal_image_ref
397
398 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000399 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000400 num_creates_expected_on_updt=0,
401 num_deletes_expected_on_updt=0,
402 update_replace=True)
403
404 def test_instance_group_update_replace_with_adjusted_capacity(self):
405 """
406 Test update replace with capacity adjustment due to conflict in
407 batch size and minimum instances in service.
408 """
409 updt_template = self.ig_tmpl_with_updt_policy()
410 grp = updt_template['Resources']['JobServerGroup']
411 policy = grp['UpdatePolicy']['RollingUpdate']
Angus Salkeld45a4e492015-03-05 17:55:36 +1000412 policy['MinInstancesInService'] = '4'
Angus Salkeld771235a2015-01-20 15:11:42 +1000413 policy['MaxBatchSize'] = '4'
414 config = updt_template['Resources']['JobServerConfig']
415 config['Properties']['ImageId'] = self.conf.minimal_image_ref
416
417 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000418 num_updates_expected_on_updt=2,
419 num_creates_expected_on_updt=3,
420 num_deletes_expected_on_updt=3,
Angus Salkeld771235a2015-01-20 15:11:42 +1000421 update_replace=True)
422
423 def test_instance_group_update_replace_huge_batch_size(self):
424 """
425 Test update replace with a huge batch size.
426 """
427 updt_template = self.ig_tmpl_with_updt_policy()
428 group = updt_template['Resources']['JobServerGroup']
429 policy = group['UpdatePolicy']['RollingUpdate']
430 policy['MinInstancesInService'] = '0'
431 policy['MaxBatchSize'] = '20'
432 config = updt_template['Resources']['JobServerConfig']
433 config['Properties']['ImageId'] = self.conf.minimal_image_ref
434
435 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000436 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000437 num_creates_expected_on_updt=0,
438 num_deletes_expected_on_updt=0,
439 update_replace=True)
440
441 def test_instance_group_update_replace_huge_min_in_service(self):
442 """
443 Test update replace with a huge number of minimum instances in service.
444 """
445 updt_template = self.ig_tmpl_with_updt_policy()
446 group = updt_template['Resources']['JobServerGroup']
447 policy = group['UpdatePolicy']['RollingUpdate']
448 policy['MinInstancesInService'] = '20'
Angus Salkeld45a4e492015-03-05 17:55:36 +1000449 policy['MaxBatchSize'] = '2'
Angus Salkeld771235a2015-01-20 15:11:42 +1000450 policy['PauseTime'] = 'PT0S'
451 config = updt_template['Resources']['JobServerConfig']
452 config['Properties']['ImageId'] = self.conf.minimal_image_ref
453
454 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000455 num_updates_expected_on_updt=3,
456 num_creates_expected_on_updt=2,
457 num_deletes_expected_on_updt=2,
Angus Salkeld771235a2015-01-20 15:11:42 +1000458 update_replace=True)
459
460 def test_instance_group_update_no_replace(self):
461 """
462 Test simple update only and no replace (i.e. updated instance flavor
463 in Launch Configuration) with no conflict in batch size and
464 minimum instances in service.
465 """
466 updt_template = self.ig_tmpl_with_updt_policy()
467 group = updt_template['Resources']['JobServerGroup']
468 policy = group['UpdatePolicy']['RollingUpdate']
469 policy['MinInstancesInService'] = '1'
470 policy['MaxBatchSize'] = '3'
471 policy['PauseTime'] = 'PT0S'
472 config = updt_template['Resources']['JobServerConfig']
473 config['Properties']['InstanceType'] = 'm1.tiny'
474
475 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000476 num_updates_expected_on_updt=5,
Angus Salkeld771235a2015-01-20 15:11:42 +1000477 num_creates_expected_on_updt=0,
478 num_deletes_expected_on_updt=0,
479 update_replace=False)
480
481 def test_instance_group_update_no_replace_with_adjusted_capacity(self):
482 """
483 Test update only and no replace (i.e. updated instance flavor in
484 Launch Configuration) with capacity adjustment due to conflict in
485 batch size and minimum instances in service.
486 """
487 updt_template = self.ig_tmpl_with_updt_policy()
488 group = updt_template['Resources']['JobServerGroup']
489 policy = group['UpdatePolicy']['RollingUpdate']
Angus Salkeld45a4e492015-03-05 17:55:36 +1000490 policy['MinInstancesInService'] = '4'
Angus Salkeld771235a2015-01-20 15:11:42 +1000491 policy['MaxBatchSize'] = '4'
492 policy['PauseTime'] = 'PT0S'
493 config = updt_template['Resources']['JobServerConfig']
494 config['Properties']['InstanceType'] = 'm1.tiny'
495
496 self.update_instance_group(updt_template,
Angus Salkeld45a4e492015-03-05 17:55:36 +1000497 num_updates_expected_on_updt=2,
498 num_creates_expected_on_updt=3,
499 num_deletes_expected_on_updt=3,
Angus Salkeld771235a2015-01-20 15:11:42 +1000500 update_replace=False)