blob: 626a956c6ad1ff18448c920d063836b04647c585 [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 Salkeld8c5b8552014-12-15 11:22:30 +100013import copy
Angus Salkeldebf15d72014-12-10 17:03:15 +100014import logging
Angus Salkeld8c5b8552014-12-15 11:22:30 +100015import yaml
16
17from heatclient import exc
Angus Salkeldebf15d72014-12-10 17:03:15 +100018
19from 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"},
34 "flavor": {"Type": "String"},
35 "keyname": {"Type": "String"}},
36 "Resources": {
37 "JobServerGroup": {
38 "Type": "OS::Heat::InstanceGroup",
39 "Properties": {
40 "LaunchConfigurationName" : {"Ref": "JobServerConfig"},
41 "Size" : {"Ref": "size"},
42 "AvailabilityZones" : [{"Ref": "AZ"}]
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 "KeyName" : {"Ref": "keyname"},
53 "SecurityGroups" : [ "sg-1" ],
54 "UserData" : "jsconfig data",
55 }
56 }
57 },
58 "Outputs": {
59 "InstanceList": {"Value": {
60 "Fn::GetAtt": ["JobServerGroup", "InstanceList"]}}
61 }
62}
63'''
64
65 instance_template = '''
66heat_template_version: 2013-05-23
67parameters:
68 ImageId: {type: string}
69 InstanceType: {type: string}
70 KeyName: {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
79outputs:
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}
90 KeyName: {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
Angus Salkeldebf15d72014-12-10 17:03:15 +1000111 def setUp(self):
112 super(InstanceGroupTest, 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.keypair_name:
117 raise self.skipException("No keyname configured to test")
118 if not self.conf.instance_type:
119 raise self.skipException("No flavor configured to test")
120
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000121 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
Angus Salkeldd67cf702014-12-18 10:40:47 +1000125 def _get_nested_identifier(self, stack_identifier):
126 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
127 nested_link = [l for l in rsrc.links if l['rel'] == 'nested']
128 self.assertEqual(1, len(nested_link))
129 nested_href = nested_link[0]['href']
130 nested_id = nested_href.split('/')[-1]
131 nested_identifier = '/'.join(nested_href.split('/')[-2:])
132 physical_resource_id = rsrc.physical_resource_id
133 self.assertEqual(physical_resource_id, nested_id)
134 return nested_identifier
135
136 def _assert_instance_state(self, nested_identifier,
137 num_complete, num_failed):
138 for res in self.client.resources.list(nested_identifier):
139 if 'COMPLETE' in res.resource_status:
140 num_complete = num_complete - 1
141 elif 'FAILED' in res.resource_status:
142 num_failed = num_failed - 1
143 self.assertEqual(0, num_failed)
144 self.assertEqual(0, num_complete)
145
Angus Salkeldebf15d72014-12-10 17:03:15 +1000146 def test_basic_create_works(self):
147 """Make sure the working case is good.
148 Note this combines test_override_aws_ec2_instance into this test as
149 well, which is:
150 If AWS::EC2::Instance is overridden, InstanceGroup will automatically
151 use that overridden resource type.
152 """
153
Angus Salkeldebf15d72014-12-10 17:03:15 +1000154 files = {'provider.yaml': self.instance_template}
155 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
156 'parameters': {'size': 4,
157 'image': self.conf.image_ref,
158 'keyname': self.conf.keypair_name,
159 'flavor': self.conf.instance_type}}
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000160 stack_identifier = self.stack_create(template=self.template,
161 files=files, environment=env)
Angus Salkeldebf15d72014-12-10 17:03:15 +1000162 initial_resources = {
163 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
164 'JobServerGroup': 'OS::Heat::InstanceGroup'}
165 self.assertEqual(initial_resources,
166 self.list_resources(stack_identifier))
167
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000168 stack = self.client.stacks.get(stack_identifier)
169 self.assert_instance_count(stack, 4)
170
Angus Salkeld8c5b8552014-12-15 11:22:30 +1000171 def test_create_config_prop_validation(self):
172 """Make sure that during a group create the instance
173 properties are validated. And an error causes the group to fail.
174 """
175 stack_name = self._stack_rand_name()
176
177 # add a property without a default and don't provide a value.
178 # we use this to make the instance fail on a property validation
179 # error.
180 broken = yaml.load(copy.copy(self.instance_template))
181 broken['parameters']['no_default'] = {'type': 'string'}
182 files = {'provider.yaml': yaml.dump(broken)}
183 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
184 'parameters': {'size': 4,
185 'image': self.conf.image_ref,
186 'keyname': self.conf.keypair_name,
187 'flavor': self.conf.instance_type}}
188
189 # now with static nested stack validation, this gets raised quickly.
190 excp = self.assertRaises(exc.HTTPBadRequest, self.client.stacks.create,
191 stack_name=stack_name, template=self.template,
192 files=files, disable_rollback=True,
193 parameters={}, environment=env)
194 self.assertIn('Property no_default not assigned', str(excp))
195
Angus Salkeldbfc7e932014-12-15 11:15:45 +1000196 def test_size_updates_work(self):
197 files = {'provider.yaml': self.instance_template}
198 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
199 'parameters': {'size': 2,
200 'image': self.conf.image_ref,
201 'keyname': self.conf.keypair_name,
202 'flavor': self.conf.instance_type}}
203
204 stack_identifier = self.stack_create(template=self.template,
205 files=files,
206 environment=env)
207 stack = self.client.stacks.get(stack_identifier)
208 self.assert_instance_count(stack, 2)
209
210 # Increase min size to 5
211 env2 = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
212 'parameters': {'size': 5,
213 'image': self.conf.image_ref,
214 'keyname': self.conf.keypair_name,
215 'flavor': self.conf.instance_type}}
216 self.update_stack(stack_identifier, self.template,
217 environment=env2, files=files)
218 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
219 stack = self.client.stacks.get(stack_identifier)
220 self.assert_instance_count(stack, 5)
Angus Salkeldcd21b1b2014-12-15 11:27:04 +1000221
222 def test_update_group_replace(self):
223 """Make sure that during a group update the non updatable
224 properties cause a replacement.
225 """
226 files = {'provider.yaml': self.instance_template}
227 env = {'resource_registry':
228 {'AWS::EC2::Instance': 'provider.yaml'},
229 'parameters': {'size': 1,
230 'image': self.conf.image_ref,
231 'keyname': self.conf.keypair_name,
232 'flavor': self.conf.instance_type}}
233
234 stack_identifier = self.stack_create(template=self.template,
235 files=files,
236 environment=env)
237 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
238 orig_asg_id = rsrc.physical_resource_id
239
240 env2 = {'resource_registry':
241 {'AWS::EC2::Instance': 'provider.yaml'},
242 'parameters': {'size': '2',
243 'AZ': 'wibble',
244 'image': self.conf.image_ref,
245 'keyname': self.conf.keypair_name,
246 'flavor': self.conf.instance_type}}
247 self.update_stack(stack_identifier, self.template,
248 environment=env2, files=files)
249
250 # replacement will cause the resource physical_resource_id to change.
251 rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
252 self.assertNotEqual(orig_asg_id, rsrc.physical_resource_id)
Angus Salkeldd67cf702014-12-18 10:40:47 +1000253
254 def test_create_instance_error_causes_group_error(self):
255 """If a resource in an instance group fails to be created, the instance
256 group itself will fail and the broken inner resource will remain.
257 """
258 stack_name = self._stack_rand_name()
259 files = {'provider.yaml': self.bad_instance_template}
260 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
261 'parameters': {'size': 2,
262 'image': self.conf.image_ref,
263 'keyname': self.conf.keypair_name,
264 'flavor': self.conf.instance_type}}
265
266 self.client.stacks.create(
267 stack_name=stack_name,
268 template=self.template,
269 files=files,
270 disable_rollback=True,
271 parameters={},
272 environment=env
273 )
274 self.addCleanup(self.client.stacks.delete, stack_name)
275 stack = self.client.stacks.get(stack_name)
276 stack_identifier = '%s/%s' % (stack_name, stack.id)
277 self._wait_for_stack_status(stack_identifier, 'CREATE_FAILED')
278 initial_resources = {
279 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
280 'JobServerGroup': 'OS::Heat::InstanceGroup'}
281 self.assertEqual(initial_resources,
282 self.list_resources(stack_identifier))
283
284 nested_ident = self._get_nested_identifier(stack_identifier)
285 self._assert_instance_state(nested_ident, 0, 2)
286
287 def test_update_instance_error_causes_group_error(self):
288 """If a resource in an instance group fails to be created during an
289 update, the instance group itself will fail and the broken inner
290 resource will remain.
291 """
292 files = {'provider.yaml': self.instance_template}
293 env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
294 'parameters': {'size': 2,
295 'image': self.conf.image_ref,
296 'keyname': self.conf.keypair_name,
297 'flavor': self.conf.instance_type}}
298
299 stack_identifier = self.stack_create(template=self.template,
300 files=files,
301 environment=env)
302 initial_resources = {
303 'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
304 'JobServerGroup': 'OS::Heat::InstanceGroup'}
305 self.assertEqual(initial_resources,
306 self.list_resources(stack_identifier))
307
308 stack = self.client.stacks.get(stack_identifier)
309 self.assert_instance_count(stack, 2)
310 nested_ident = self._get_nested_identifier(stack_identifier)
311 self._assert_instance_state(nested_ident, 2, 0)
312
313 env['parameters']['size'] = 3
314 files2 = {'provider.yaml': self.bad_instance_template}
315 self.client.stacks.update(
316 stack_id=stack_identifier,
317 template=self.template,
318 files=files2,
319 disable_rollback=True,
320 parameters={},
321 environment=env
322 )
323 self._wait_for_stack_status(stack_identifier, 'UPDATE_FAILED')
324
325 # assert that there are 3 bad instances
326 nested_ident = self._get_nested_identifier(stack_identifier)
327 self._assert_instance_state(nested_ident, 0, 3)