blob: 4c9656737a1e13419ff2773f57ac88aa55d59aa5 [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 Salkeld24043702014-11-21 08:49:26 +100088 self.useFixture(fixtures.FakeLogger(format=_LOG_FORMAT))
Steve Baker450aa7f2014-08-25 10:37:27 +120089
90 def status_timeout(self, things, thing_id, expected_status,
91 error_status='ERROR',
92 not_found_exception=heat_exceptions.NotFound):
93 """
94 Given a thing and an expected status, do a loop, sleeping
95 for a configurable amount of time, checking for the
96 expected status to show. At any time, if the returned
97 status of the thing is ERROR, fail out.
98 """
99 self._status_timeout(things, thing_id,
100 expected_status=expected_status,
101 error_status=error_status,
102 not_found_exception=not_found_exception)
103
104 def _status_timeout(self,
105 things,
106 thing_id,
107 expected_status=None,
108 allow_notfound=False,
109 error_status='ERROR',
110 not_found_exception=heat_exceptions.NotFound):
111
112 log_status = expected_status if expected_status else ''
113 if allow_notfound:
114 log_status += ' or NotFound' if log_status != '' else 'NotFound'
115
116 def check_status():
117 # python-novaclient has resources available to its client
118 # that all implement a get() method taking an identifier
119 # for the singular resource to retrieve.
120 try:
121 thing = things.get(thing_id)
122 except not_found_exception:
123 if allow_notfound:
124 return True
125 raise
126 except Exception as e:
127 if allow_notfound and self.not_found_exception(e):
128 return True
129 raise
130
131 new_status = thing.status
132
133 # Some components are reporting error status in lower case
134 # so case sensitive comparisons can really mess things
135 # up.
136 if new_status.lower() == error_status.lower():
137 message = ("%s failed to get to expected status (%s). "
138 "In %s state.") % (thing, expected_status,
139 new_status)
140 raise exceptions.BuildErrorException(message,
141 server_id=thing_id)
142 elif new_status == expected_status and expected_status is not None:
143 return True # All good.
144 LOG.debug("Waiting for %s to get to %s status. "
145 "Currently in %s status",
146 thing, log_status, new_status)
147 if not call_until_true(
148 check_status,
149 self.conf.build_timeout,
150 self.conf.build_interval):
151 message = ("Timed out waiting for thing %s "
152 "to become %s") % (thing_id, log_status)
153 raise exceptions.TimeoutException(message)
154
155 def get_remote_client(self, server_or_ip, username, private_key=None):
156 if isinstance(server_or_ip, six.string_types):
157 ip = server_or_ip
158 else:
159 network_name_for_ssh = self.conf.network_for_ssh
160 ip = server_or_ip.networks[network_name_for_ssh][0]
161 if private_key is None:
162 private_key = self.keypair.private_key
163 linux_client = remote_client.RemoteClient(ip, username,
164 pkey=private_key,
165 conf=self.conf)
166 try:
167 linux_client.validate_authentication()
168 except exceptions.SSHTimeout:
169 LOG.exception('ssh connection to %s failed' % ip)
170 raise
171
172 return linux_client
173
174 def _log_console_output(self, servers=None):
175 if not servers:
176 servers = self.compute_client.servers.list()
177 for server in servers:
178 LOG.debug('Console output for %s', server.id)
179 LOG.debug(server.get_console_output())
180
181 def _load_template(self, base_file, file_name):
182 filepath = os.path.join(os.path.dirname(os.path.realpath(base_file)),
183 file_name)
184 with open(filepath) as f:
185 return f.read()
186
187 def create_keypair(self, client=None, name=None):
188 if client is None:
189 client = self.compute_client
190 if name is None:
191 name = rand_name('heat-keypair')
192 keypair = client.keypairs.create(name)
193 self.assertEqual(keypair.name, name)
194
195 def delete_keypair():
196 keypair.delete()
197
198 self.addCleanup(delete_keypair)
199 return keypair
200
201 @classmethod
202 def _stack_rand_name(cls):
203 return rand_name(cls.__name__)
204
205 def _get_default_network(self):
206 networks = self.network_client.list_networks()
207 for net in networks['networks']:
208 if net['name'] == self.conf.fixed_network_name:
209 return net
210
211 @staticmethod
212 def _stack_output(stack, output_key):
213 """Return a stack output value for a given key."""
214 return next((o['output_value'] for o in stack.outputs
215 if o['output_key'] == output_key), None)
216
217 def _ping_ip_address(self, ip_address, should_succeed=True):
218 cmd = ['ping', '-c1', '-w1', ip_address]
219
220 def ping():
221 proc = subprocess.Popen(cmd,
222 stdout=subprocess.PIPE,
223 stderr=subprocess.PIPE)
224 proc.wait()
225 return (proc.returncode == 0) == should_succeed
226
227 return call_until_true(
228 ping, self.conf.build_timeout, 1)
229
230 def _wait_for_resource_status(self, stack_identifier, resource_name,
231 status, failure_pattern='^.*_FAILED$',
232 success_on_not_found=False):
233 """Waits for a Resource to reach a given status."""
234 fail_regexp = re.compile(failure_pattern)
235 build_timeout = self.conf.build_timeout
236 build_interval = self.conf.build_interval
237
238 start = timeutils.utcnow()
239 while timeutils.delta_seconds(start,
240 timeutils.utcnow()) < build_timeout:
241 try:
242 res = self.client.resources.get(
243 stack_identifier, resource_name)
244 except heat_exceptions.HTTPNotFound:
245 if success_on_not_found:
246 return
247 # ignore this, as the resource may not have
248 # been created yet
249 else:
250 if res.resource_status == status:
251 return
252 if fail_regexp.search(res.resource_status):
253 raise exceptions.StackResourceBuildErrorException(
254 resource_name=res.resource_name,
255 stack_identifier=stack_identifier,
256 resource_status=res.resource_status,
257 resource_status_reason=res.resource_status_reason)
258 time.sleep(build_interval)
259
260 message = ('Resource %s failed to reach %s status within '
261 'the required time (%s s).' %
262 (res.resource_name, status, build_timeout))
263 raise exceptions.TimeoutException(message)
264
265 def _wait_for_stack_status(self, stack_identifier, status,
266 failure_pattern='^.*_FAILED$',
267 success_on_not_found=False):
268 """
269 Waits for a Stack to reach a given status.
270
271 Note this compares the full $action_$status, e.g
272 CREATE_COMPLETE, not just COMPLETE which is exposed
273 via the status property of Stack in heatclient
274 """
275 fail_regexp = re.compile(failure_pattern)
276 build_timeout = self.conf.build_timeout
277 build_interval = self.conf.build_interval
278
279 start = timeutils.utcnow()
280 while timeutils.delta_seconds(start,
281 timeutils.utcnow()) < build_timeout:
282 try:
283 stack = self.client.stacks.get(stack_identifier)
284 except heat_exceptions.HTTPNotFound:
285 if success_on_not_found:
286 return
287 # ignore this, as the resource may not have
288 # been created yet
289 else:
290 if stack.stack_status == status:
291 return
292 if fail_regexp.search(stack.stack_status):
293 raise exceptions.StackBuildErrorException(
294 stack_identifier=stack_identifier,
295 stack_status=stack.stack_status,
296 stack_status_reason=stack.stack_status_reason)
297 time.sleep(build_interval)
298
299 message = ('Stack %s failed to reach %s status within '
300 'the required time (%s s).' %
301 (stack.stack_name, status, build_timeout))
302 raise exceptions.TimeoutException(message)
303
304 def _stack_delete(self, stack_identifier):
305 try:
306 self.client.stacks.delete(stack_identifier)
307 except heat_exceptions.HTTPNotFound:
308 pass
309 self._wait_for_stack_status(
310 stack_identifier, 'DELETE_COMPLETE',
311 success_on_not_found=True)
Steven Hardyc9efd972014-11-20 11:31:55 +0000312
313 def update_stack(self, stack_identifier, template, environment=None,
314 files=None):
315 env = environment or {}
316 env_files = files or {}
317 stack_name = stack_identifier.split('/')[0]
318 self.client.stacks.update(
319 stack_id=stack_identifier,
320 stack_name=stack_name,
321 template=template,
322 files=env_files,
323 disable_rollback=True,
324 parameters={},
325 environment=env
326 )
327 self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
328
Angus Salkeld2bd63a42015-01-07 11:11:29 +1000329 def assert_resource_is_a_stack(self, stack_identifier, res_name):
330 rsrc = self.client.resources.get(stack_identifier, res_name)
331 nested_link = [l for l in rsrc.links if l['rel'] == 'nested']
332 nested_href = nested_link[0]['href']
333 nested_id = nested_href.split('/')[-1]
334 nested_identifier = '/'.join(nested_href.split('/')[-2:])
335 self.assertEqual(rsrc.physical_resource_id, nested_id)
336
337 nested_stack = self.client.stacks.get(nested_id)
338 nested_identifier2 = '%s/%s' % (nested_stack.stack_name,
339 nested_stack.id)
340 self.assertEqual(nested_identifier, nested_identifier2)
341 parent_id = stack_identifier.split("/")[-1]
342 self.assertEqual(parent_id, nested_stack.parent)
343 return nested_identifier
344
Steven Hardyc9efd972014-11-20 11:31:55 +0000345 def list_resources(self, stack_identifier):
346 resources = self.client.resources.list(stack_identifier)
347 return dict((r.resource_name, r.resource_type) for r in resources)
Steven Hardyf2c82c02014-11-20 14:02:17 +0000348
349 def stack_create(self, stack_name=None, template=None, files=None,
Steven Hardy7c1f2242015-01-12 16:32:56 +0000350 parameters=None, environment=None,
351 expected_status='CREATE_COMPLETE'):
Steven Hardyf2c82c02014-11-20 14:02:17 +0000352 name = stack_name or self._stack_rand_name()
353 templ = template or self.template
354 templ_files = files or {}
355 params = parameters or {}
356 env = environment or {}
357 self.client.stacks.create(
358 stack_name=name,
359 template=templ,
360 files=templ_files,
361 disable_rollback=True,
362 parameters=params,
363 environment=env
364 )
365 self.addCleanup(self.client.stacks.delete, name)
366
367 stack = self.client.stacks.get(name)
368 stack_identifier = '%s/%s' % (name, stack.id)
Steven Hardy7c1f2242015-01-12 16:32:56 +0000369 self._wait_for_stack_status(stack_identifier, expected_status)
Steven Hardyf2c82c02014-11-20 14:02:17 +0000370 return stack_identifier
Angus Salkeld2bd63a42015-01-07 11:11:29 +1000371
372 def stack_adopt(self, stack_name=None, files=None,
373 parameters=None, environment=None, adopt_data=None,
374 wait_for_status='ADOPT_COMPLETE'):
375 name = stack_name or self._stack_rand_name()
376 templ_files = files or {}
377 params = parameters or {}
378 env = environment or {}
379 self.client.stacks.create(
380 stack_name=name,
381 files=templ_files,
382 disable_rollback=True,
383 parameters=params,
384 environment=env,
385 adopt_stack_data=adopt_data,
386 )
387 self.addCleanup(self.client.stacks.delete, name)
388
389 stack = self.client.stacks.get(name)
390 stack_identifier = '%s/%s' % (name, stack.id)
391 self._wait_for_stack_status(stack_identifier, wait_for_status)
392 return stack_identifier