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