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