blob: bfb12605eca06320507b2dfa024116fadd976cef [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
Sergey Krayneva265c132015-02-13 03:51:03 -0500203 def assign_keypair(self):
204 if self.conf.keypair_name:
205 self.keypair = None
206 self.keypair_name = self.conf.keypair_name
207 else:
208 self.keypair = self.create_keypair()
209 self.keypair_name = self.keypair.id
210
Steve Baker450aa7f2014-08-25 10:37:27 +1200211 @classmethod
212 def _stack_rand_name(cls):
213 return rand_name(cls.__name__)
214
215 def _get_default_network(self):
216 networks = self.network_client.list_networks()
217 for net in networks['networks']:
218 if net['name'] == self.conf.fixed_network_name:
219 return net
220
221 @staticmethod
222 def _stack_output(stack, output_key):
223 """Return a stack output value for a given key."""
224 return next((o['output_value'] for o in stack.outputs
225 if o['output_key'] == output_key), None)
226
227 def _ping_ip_address(self, ip_address, should_succeed=True):
228 cmd = ['ping', '-c1', '-w1', ip_address]
229
230 def ping():
231 proc = subprocess.Popen(cmd,
232 stdout=subprocess.PIPE,
233 stderr=subprocess.PIPE)
234 proc.wait()
235 return (proc.returncode == 0) == should_succeed
236
237 return call_until_true(
238 ping, self.conf.build_timeout, 1)
239
240 def _wait_for_resource_status(self, stack_identifier, resource_name,
241 status, failure_pattern='^.*_FAILED$',
242 success_on_not_found=False):
243 """Waits for a Resource to reach a given status."""
244 fail_regexp = re.compile(failure_pattern)
245 build_timeout = self.conf.build_timeout
246 build_interval = self.conf.build_interval
247
248 start = timeutils.utcnow()
249 while timeutils.delta_seconds(start,
250 timeutils.utcnow()) < build_timeout:
251 try:
252 res = self.client.resources.get(
253 stack_identifier, resource_name)
254 except heat_exceptions.HTTPNotFound:
255 if success_on_not_found:
256 return
257 # ignore this, as the resource may not have
258 # been created yet
259 else:
260 if res.resource_status == status:
261 return
262 if fail_regexp.search(res.resource_status):
263 raise exceptions.StackResourceBuildErrorException(
264 resource_name=res.resource_name,
265 stack_identifier=stack_identifier,
266 resource_status=res.resource_status,
267 resource_status_reason=res.resource_status_reason)
268 time.sleep(build_interval)
269
270 message = ('Resource %s failed to reach %s status within '
271 'the required time (%s s).' %
272 (res.resource_name, status, build_timeout))
273 raise exceptions.TimeoutException(message)
274
275 def _wait_for_stack_status(self, stack_identifier, status,
276 failure_pattern='^.*_FAILED$',
277 success_on_not_found=False):
278 """
279 Waits for a Stack to reach a given status.
280
281 Note this compares the full $action_$status, e.g
282 CREATE_COMPLETE, not just COMPLETE which is exposed
283 via the status property of Stack in heatclient
284 """
285 fail_regexp = re.compile(failure_pattern)
286 build_timeout = self.conf.build_timeout
287 build_interval = self.conf.build_interval
288
289 start = timeutils.utcnow()
290 while timeutils.delta_seconds(start,
291 timeutils.utcnow()) < build_timeout:
292 try:
293 stack = self.client.stacks.get(stack_identifier)
294 except heat_exceptions.HTTPNotFound:
295 if success_on_not_found:
296 return
297 # ignore this, as the resource may not have
298 # been created yet
299 else:
300 if stack.stack_status == status:
301 return
302 if fail_regexp.search(stack.stack_status):
303 raise exceptions.StackBuildErrorException(
304 stack_identifier=stack_identifier,
305 stack_status=stack.stack_status,
306 stack_status_reason=stack.stack_status_reason)
307 time.sleep(build_interval)
308
309 message = ('Stack %s failed to reach %s status within '
310 'the required time (%s s).' %
311 (stack.stack_name, status, build_timeout))
312 raise exceptions.TimeoutException(message)
313
314 def _stack_delete(self, stack_identifier):
315 try:
316 self.client.stacks.delete(stack_identifier)
317 except heat_exceptions.HTTPNotFound:
318 pass
319 self._wait_for_stack_status(
320 stack_identifier, 'DELETE_COMPLETE',
321 success_on_not_found=True)
Steven Hardyc9efd972014-11-20 11:31:55 +0000322
323 def update_stack(self, stack_identifier, template, environment=None,
324 files=None):
325 env = environment or {}
326 env_files = files or {}
327 stack_name = stack_identifier.split('/')[0]
328 self.client.stacks.update(
329 stack_id=stack_identifier,
330 stack_name=stack_name,
331 template=template,
332 files=env_files,
333 disable_rollback=True,
334 parameters={},
335 environment=env
336 )
337 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
338
Angus Salkeld2bd63a42015-01-07 11:11:29 +1000339 def assert_resource_is_a_stack(self, stack_identifier, res_name):
340 rsrc = self.client.resources.get(stack_identifier, res_name)
341 nested_link = [l for l in rsrc.links if l['rel'] == 'nested']
342 nested_href = nested_link[0]['href']
343 nested_id = nested_href.split('/')[-1]
344 nested_identifier = '/'.join(nested_href.split('/')[-2:])
345 self.assertEqual(rsrc.physical_resource_id, nested_id)
346
347 nested_stack = self.client.stacks.get(nested_id)
348 nested_identifier2 = '%s/%s' % (nested_stack.stack_name,
349 nested_stack.id)
350 self.assertEqual(nested_identifier, nested_identifier2)
351 parent_id = stack_identifier.split("/")[-1]
352 self.assertEqual(parent_id, nested_stack.parent)
353 return nested_identifier
354
Steven Hardyc9efd972014-11-20 11:31:55 +0000355 def list_resources(self, stack_identifier):
356 resources = self.client.resources.list(stack_identifier)
357 return dict((r.resource_name, r.resource_type) for r in resources)
Steven Hardyf2c82c02014-11-20 14:02:17 +0000358
359 def stack_create(self, stack_name=None, template=None, files=None,
Steven Hardy7c1f2242015-01-12 16:32:56 +0000360 parameters=None, environment=None,
361 expected_status='CREATE_COMPLETE'):
Steven Hardyf2c82c02014-11-20 14:02:17 +0000362 name = stack_name or self._stack_rand_name()
363 templ = template or self.template
364 templ_files = files or {}
365 params = parameters or {}
366 env = environment or {}
367 self.client.stacks.create(
368 stack_name=name,
369 template=templ,
370 files=templ_files,
371 disable_rollback=True,
372 parameters=params,
373 environment=env
374 )
375 self.addCleanup(self.client.stacks.delete, name)
376
377 stack = self.client.stacks.get(name)
378 stack_identifier = '%s/%s' % (name, stack.id)
Steven Hardy7c1f2242015-01-12 16:32:56 +0000379 self._wait_for_stack_status(stack_identifier, expected_status)
Steven Hardyf2c82c02014-11-20 14:02:17 +0000380 return stack_identifier
Angus Salkeld2bd63a42015-01-07 11:11:29 +1000381
382 def stack_adopt(self, stack_name=None, files=None,
383 parameters=None, environment=None, adopt_data=None,
384 wait_for_status='ADOPT_COMPLETE'):
385 name = stack_name or self._stack_rand_name()
386 templ_files = files or {}
387 params = parameters or {}
388 env = environment or {}
389 self.client.stacks.create(
390 stack_name=name,
391 files=templ_files,
392 disable_rollback=True,
393 parameters=params,
394 environment=env,
395 adopt_stack_data=adopt_data,
396 )
397 self.addCleanup(self.client.stacks.delete, name)
398
399 stack = self.client.stacks.get(name)
400 stack_identifier = '%s/%s' % (name, stack.id)
401 self._wait_for_stack_status(stack_identifier, wait_for_status)
402 return stack_identifier