blob: 7b17d57f26e140b16ce30cffa0ab764724a70a1b [file] [log] [blame]
Steve Baker450aa7f2014-08-25 10:37:27 +12001# 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 logging
14import os
15import random
16import re
Steve Baker450aa7f2014-08-25 10:37:27 +120017import subprocess
Steve Baker450aa7f2014-08-25 10:37:27 +120018import time
19
Pavlo Shchelokovskyy60e0ecd2014-12-14 22:17:21 +020020import fixtures
Steve Baker450aa7f2014-08-25 10:37:27 +120021from heatclient import exc as heat_exceptions
Pavlo Shchelokovskyyc6b25622015-01-02 13:22:05 +020022from oslo.utils import timeutils
Pavlo Shchelokovskyy60e0ecd2014-12-14 22:17:21 +020023import six
24import testscenarios
25import testtools
Steve Baker450aa7f2014-08-25 10:37:27 +120026
Steve Baker450aa7f2014-08-25 10:37:27 +120027from heat_integrationtests.common import clients
28from heat_integrationtests.common import config
29from heat_integrationtests.common import exceptions
30from heat_integrationtests.common import remote_client
31
32LOG = logging.getLogger(__name__)
Angus Salkeld24043702014-11-21 08:49:26 +100033_LOG_FORMAT = "%(levelname)8s [%(name)s] %(message)s"
Steve Baker450aa7f2014-08-25 10:37:27 +120034
35
36def call_until_true(func, duration, sleep_for):
37 """
38 Call the given function until it returns True (and return True) or
39 until the specified duration (in seconds) elapses (and return
40 False).
41
42 :param func: A zero argument callable that returns True on success.
43 :param duration: The number of seconds for which to attempt a
44 successful call of the function.
45 :param sleep_for: The number of seconds to sleep after an unsuccessful
46 invocation of the function.
47 """
48 now = time.time()
49 timeout = now + duration
50 while now < timeout:
51 if func():
52 return True
53 LOG.debug("Sleeping for %d seconds", sleep_for)
54 time.sleep(sleep_for)
55 now = time.time()
56 return False
57
58
59def rand_name(name=''):
60 randbits = str(random.randint(1, 0x7fffffff))
61 if name:
62 return name + '-' + randbits
63 else:
64 return randbits
65
66
Angus Salkeld95f65a22014-11-24 12:38:30 +100067class HeatIntegrationTest(testscenarios.WithScenarios,
68 testtools.TestCase):
Steve Baker450aa7f2014-08-25 10:37:27 +120069
70 def setUp(self):
71 super(HeatIntegrationTest, self).setUp()
72
73 self.conf = config.init_conf()
74
75 self.assertIsNotNone(self.conf.auth_url,
76 'No auth_url configured')
77 self.assertIsNotNone(self.conf.username,
78 'No username configured')
79 self.assertIsNotNone(self.conf.password,
80 'No password configured')
81
82 self.manager = clients.ClientManager(self.conf)
83 self.identity_client = self.manager.identity_client
84 self.orchestration_client = self.manager.orchestration_client
85 self.compute_client = self.manager.compute_client
86 self.network_client = self.manager.network_client
87 self.volume_client = self.manager.volume_client
Angus Salkeld4408da32015-02-03 18:53:30 +100088 self.object_client = self.manager.object_client
Angus Salkeld24043702014-11-21 08:49:26 +100089 self.useFixture(fixtures.FakeLogger(format=_LOG_FORMAT))
Steve Baker450aa7f2014-08-25 10:37:27 +120090
91 def status_timeout(self, things, thing_id, expected_status,
92 error_status='ERROR',
93 not_found_exception=heat_exceptions.NotFound):
94 """
95 Given a thing and an expected status, do a loop, sleeping
96 for a configurable amount of time, checking for the
97 expected status to show. At any time, if the returned
98 status of the thing is ERROR, fail out.
99 """
100 self._status_timeout(things, thing_id,
101 expected_status=expected_status,
102 error_status=error_status,
103 not_found_exception=not_found_exception)
104
105 def _status_timeout(self,
106 things,
107 thing_id,
108 expected_status=None,
109 allow_notfound=False,
110 error_status='ERROR',
111 not_found_exception=heat_exceptions.NotFound):
112
113 log_status = expected_status if expected_status else ''
114 if allow_notfound:
115 log_status += ' or NotFound' if log_status != '' else 'NotFound'
116
117 def check_status():
118 # python-novaclient has resources available to its client
119 # that all implement a get() method taking an identifier
120 # for the singular resource to retrieve.
121 try:
122 thing = things.get(thing_id)
123 except not_found_exception:
124 if allow_notfound:
125 return True
126 raise
127 except Exception as e:
128 if allow_notfound and self.not_found_exception(e):
129 return True
130 raise
131
132 new_status = thing.status
133
134 # Some components are reporting error status in lower case
135 # so case sensitive comparisons can really mess things
136 # up.
137 if new_status.lower() == error_status.lower():
138 message = ("%s failed to get to expected status (%s). "
139 "In %s state.") % (thing, expected_status,
140 new_status)
141 raise exceptions.BuildErrorException(message,
142 server_id=thing_id)
143 elif new_status == expected_status and expected_status is not None:
144 return True # All good.
145 LOG.debug("Waiting for %s to get to %s status. "
146 "Currently in %s status",
147 thing, log_status, new_status)
148 if not call_until_true(
149 check_status,
150 self.conf.build_timeout,
151 self.conf.build_interval):
152 message = ("Timed out waiting for thing %s "
153 "to become %s") % (thing_id, log_status)
154 raise exceptions.TimeoutException(message)
155
156 def get_remote_client(self, server_or_ip, username, private_key=None):
157 if isinstance(server_or_ip, six.string_types):
158 ip = server_or_ip
159 else:
160 network_name_for_ssh = self.conf.network_for_ssh
161 ip = server_or_ip.networks[network_name_for_ssh][0]
162 if private_key is None:
163 private_key = self.keypair.private_key
164 linux_client = remote_client.RemoteClient(ip, username,
165 pkey=private_key,
166 conf=self.conf)
167 try:
168 linux_client.validate_authentication()
169 except exceptions.SSHTimeout:
170 LOG.exception('ssh connection to %s failed' % ip)
171 raise
172
173 return linux_client
174
175 def _log_console_output(self, servers=None):
176 if not servers:
177 servers = self.compute_client.servers.list()
178 for server in servers:
179 LOG.debug('Console output for %s', server.id)
180 LOG.debug(server.get_console_output())
181
Sergey Kraynevd6fa5c02015-02-13 03:03:55 -0500182 def _load_template(self, base_file, file_name, sub_dir=None):
183 sub_dir = sub_dir or ''
Steve Baker450aa7f2014-08-25 10:37:27 +1200184 filepath = os.path.join(os.path.dirname(os.path.realpath(base_file)),
Sergey Kraynevd6fa5c02015-02-13 03:03:55 -0500185 sub_dir, file_name)
Steve Baker450aa7f2014-08-25 10:37:27 +1200186 with open(filepath) as f:
187 return f.read()
188
189 def create_keypair(self, client=None, name=None):
190 if client is None:
191 client = self.compute_client
192 if name is None:
193 name = rand_name('heat-keypair')
194 keypair = client.keypairs.create(name)
195 self.assertEqual(keypair.name, name)
196
197 def delete_keypair():
198 keypair.delete()
199
200 self.addCleanup(delete_keypair)
201 return keypair
202
203 @classmethod
204 def _stack_rand_name(cls):
205 return rand_name(cls.__name__)
206
207 def _get_default_network(self):
208 networks = self.network_client.list_networks()
209 for net in networks['networks']:
210 if net['name'] == self.conf.fixed_network_name:
211 return net
212
213 @staticmethod
214 def _stack_output(stack, output_key):
215 """Return a stack output value for a given key."""
216 return next((o['output_value'] for o in stack.outputs
217 if o['output_key'] == output_key), None)
218
219 def _ping_ip_address(self, ip_address, should_succeed=True):
220 cmd = ['ping', '-c1', '-w1', ip_address]
221
222 def ping():
223 proc = subprocess.Popen(cmd,
224 stdout=subprocess.PIPE,
225 stderr=subprocess.PIPE)
226 proc.wait()
227 return (proc.returncode == 0) == should_succeed
228
229 return call_until_true(
230 ping, self.conf.build_timeout, 1)
231
232 def _wait_for_resource_status(self, stack_identifier, resource_name,
233 status, failure_pattern='^.*_FAILED$',
234 success_on_not_found=False):
235 """Waits for a Resource to reach a given status."""
236 fail_regexp = re.compile(failure_pattern)
237 build_timeout = self.conf.build_timeout
238 build_interval = self.conf.build_interval
239
240 start = timeutils.utcnow()
241 while timeutils.delta_seconds(start,
242 timeutils.utcnow()) < build_timeout:
243 try:
244 res = self.client.resources.get(
245 stack_identifier, resource_name)
246 except heat_exceptions.HTTPNotFound:
247 if success_on_not_found:
248 return
249 # ignore this, as the resource may not have
250 # been created yet
251 else:
252 if res.resource_status == status:
253 return
254 if fail_regexp.search(res.resource_status):
255 raise exceptions.StackResourceBuildErrorException(
256 resource_name=res.resource_name,
257 stack_identifier=stack_identifier,
258 resource_status=res.resource_status,
259 resource_status_reason=res.resource_status_reason)
260 time.sleep(build_interval)
261
262 message = ('Resource %s failed to reach %s status within '
263 'the required time (%s s).' %
264 (res.resource_name, status, build_timeout))
265 raise exceptions.TimeoutException(message)
266
267 def _wait_for_stack_status(self, stack_identifier, status,
268 failure_pattern='^.*_FAILED$',
269 success_on_not_found=False):
270 """
271 Waits for a Stack to reach a given status.
272
273 Note this compares the full $action_$status, e.g
274 CREATE_COMPLETE, not just COMPLETE which is exposed
275 via the status property of Stack in heatclient
276 """
277 fail_regexp = re.compile(failure_pattern)
278 build_timeout = self.conf.build_timeout
279 build_interval = self.conf.build_interval
280
281 start = timeutils.utcnow()
282 while timeutils.delta_seconds(start,
283 timeutils.utcnow()) < build_timeout:
284 try:
285 stack = self.client.stacks.get(stack_identifier)
286 except heat_exceptions.HTTPNotFound:
287 if success_on_not_found:
288 return
289 # ignore this, as the resource may not have
290 # been created yet
291 else:
292 if stack.stack_status == status:
293 return
294 if fail_regexp.search(stack.stack_status):
295 raise exceptions.StackBuildErrorException(
296 stack_identifier=stack_identifier,
297 stack_status=stack.stack_status,
298 stack_status_reason=stack.stack_status_reason)
299 time.sleep(build_interval)
300
301 message = ('Stack %s failed to reach %s status within '
302 'the required time (%s s).' %
303 (stack.stack_name, status, build_timeout))
304 raise exceptions.TimeoutException(message)
305
306 def _stack_delete(self, stack_identifier):
307 try:
308 self.client.stacks.delete(stack_identifier)
309 except heat_exceptions.HTTPNotFound:
310 pass
311 self._wait_for_stack_status(
312 stack_identifier, 'DELETE_COMPLETE',
313 success_on_not_found=True)
Steven Hardyc9efd972014-11-20 11:31:55 +0000314
315 def update_stack(self, stack_identifier, template, environment=None,
316 files=None):
317 env = environment or {}
318 env_files = files or {}
319 stack_name = stack_identifier.split('/')[0]
320 self.client.stacks.update(
321 stack_id=stack_identifier,
322 stack_name=stack_name,
323 template=template,
324 files=env_files,
325 disable_rollback=True,
326 parameters={},
327 environment=env
328 )
329 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
330
Angus Salkeld2bd63a42015-01-07 11:11:29 +1000331 def assert_resource_is_a_stack(self, stack_identifier, res_name):
332 rsrc = self.client.resources.get(stack_identifier, res_name)
333 nested_link = [l for l in rsrc.links if l['rel'] == 'nested']
334 nested_href = nested_link[0]['href']
335 nested_id = nested_href.split('/')[-1]
336 nested_identifier = '/'.join(nested_href.split('/')[-2:])
337 self.assertEqual(rsrc.physical_resource_id, nested_id)
338
339 nested_stack = self.client.stacks.get(nested_id)
340 nested_identifier2 = '%s/%s' % (nested_stack.stack_name,
341 nested_stack.id)
342 self.assertEqual(nested_identifier, nested_identifier2)
343 parent_id = stack_identifier.split("/")[-1]
344 self.assertEqual(parent_id, nested_stack.parent)
345 return nested_identifier
346
Steven Hardyc9efd972014-11-20 11:31:55 +0000347 def list_resources(self, stack_identifier):
348 resources = self.client.resources.list(stack_identifier)
349 return dict((r.resource_name, r.resource_type) for r in resources)
Steven Hardyf2c82c02014-11-20 14:02:17 +0000350
351 def stack_create(self, stack_name=None, template=None, files=None,
Steven Hardy7c1f2242015-01-12 16:32:56 +0000352 parameters=None, environment=None,
353 expected_status='CREATE_COMPLETE'):
Steven Hardyf2c82c02014-11-20 14:02:17 +0000354 name = stack_name or self._stack_rand_name()
355 templ = template or self.template
356 templ_files = files or {}
357 params = parameters or {}
358 env = environment or {}
359 self.client.stacks.create(
360 stack_name=name,
361 template=templ,
362 files=templ_files,
363 disable_rollback=True,
364 parameters=params,
365 environment=env
366 )
367 self.addCleanup(self.client.stacks.delete, name)
368
369 stack = self.client.stacks.get(name)
370 stack_identifier = '%s/%s' % (name, stack.id)
Steven Hardy7c1f2242015-01-12 16:32:56 +0000371 self._wait_for_stack_status(stack_identifier, expected_status)
Steven Hardyf2c82c02014-11-20 14:02:17 +0000372 return stack_identifier
Angus Salkeld2bd63a42015-01-07 11:11:29 +1000373
374 def stack_adopt(self, stack_name=None, files=None,
375 parameters=None, environment=None, adopt_data=None,
376 wait_for_status='ADOPT_COMPLETE'):
377 name = stack_name or self._stack_rand_name()
378 templ_files = files or {}
379 params = parameters or {}
380 env = environment or {}
381 self.client.stacks.create(
382 stack_name=name,
383 files=templ_files,
384 disable_rollback=True,
385 parameters=params,
386 environment=env,
387 adopt_stack_data=adopt_data,
388 )
389 self.addCleanup(self.client.stacks.delete, name)
390
391 stack = self.client.stacks.get(name)
392 stack_identifier = '%s/%s' % (name, stack.id)
393 self._wait_for_stack_status(stack_identifier, wait_for_status)
394 return stack_identifier