blob: 29739649df1e66718fa45fb7410eb3504d75392d [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
17import six
18import subprocess
19import testtools
20import time
21
22from heatclient import exc as heat_exceptions
23
24from heat.openstack.common import timeutils
25from heat_integrationtests.common import clients
26from heat_integrationtests.common import config
27from heat_integrationtests.common import exceptions
28from heat_integrationtests.common import remote_client
29
30LOG = logging.getLogger(__name__)
31
32
33def call_until_true(func, duration, sleep_for):
34 """
35 Call the given function until it returns True (and return True) or
36 until the specified duration (in seconds) elapses (and return
37 False).
38
39 :param func: A zero argument callable that returns True on success.
40 :param duration: The number of seconds for which to attempt a
41 successful call of the function.
42 :param sleep_for: The number of seconds to sleep after an unsuccessful
43 invocation of the function.
44 """
45 now = time.time()
46 timeout = now + duration
47 while now < timeout:
48 if func():
49 return True
50 LOG.debug("Sleeping for %d seconds", sleep_for)
51 time.sleep(sleep_for)
52 now = time.time()
53 return False
54
55
56def rand_name(name=''):
57 randbits = str(random.randint(1, 0x7fffffff))
58 if name:
59 return name + '-' + randbits
60 else:
61 return randbits
62
63
64class HeatIntegrationTest(testtools.TestCase):
65
66 def setUp(self):
67 super(HeatIntegrationTest, self).setUp()
68
69 self.conf = config.init_conf()
70
71 self.assertIsNotNone(self.conf.auth_url,
72 'No auth_url configured')
73 self.assertIsNotNone(self.conf.username,
74 'No username configured')
75 self.assertIsNotNone(self.conf.password,
76 'No password configured')
77
78 self.manager = clients.ClientManager(self.conf)
79 self.identity_client = self.manager.identity_client
80 self.orchestration_client = self.manager.orchestration_client
81 self.compute_client = self.manager.compute_client
82 self.network_client = self.manager.network_client
83 self.volume_client = self.manager.volume_client
84
85 def status_timeout(self, things, thing_id, expected_status,
86 error_status='ERROR',
87 not_found_exception=heat_exceptions.NotFound):
88 """
89 Given a thing and an expected status, do a loop, sleeping
90 for a configurable amount of time, checking for the
91 expected status to show. At any time, if the returned
92 status of the thing is ERROR, fail out.
93 """
94 self._status_timeout(things, thing_id,
95 expected_status=expected_status,
96 error_status=error_status,
97 not_found_exception=not_found_exception)
98
99 def _status_timeout(self,
100 things,
101 thing_id,
102 expected_status=None,
103 allow_notfound=False,
104 error_status='ERROR',
105 not_found_exception=heat_exceptions.NotFound):
106
107 log_status = expected_status if expected_status else ''
108 if allow_notfound:
109 log_status += ' or NotFound' if log_status != '' else 'NotFound'
110
111 def check_status():
112 # python-novaclient has resources available to its client
113 # that all implement a get() method taking an identifier
114 # for the singular resource to retrieve.
115 try:
116 thing = things.get(thing_id)
117 except not_found_exception:
118 if allow_notfound:
119 return True
120 raise
121 except Exception as e:
122 if allow_notfound and self.not_found_exception(e):
123 return True
124 raise
125
126 new_status = thing.status
127
128 # Some components are reporting error status in lower case
129 # so case sensitive comparisons can really mess things
130 # up.
131 if new_status.lower() == error_status.lower():
132 message = ("%s failed to get to expected status (%s). "
133 "In %s state.") % (thing, expected_status,
134 new_status)
135 raise exceptions.BuildErrorException(message,
136 server_id=thing_id)
137 elif new_status == expected_status and expected_status is not None:
138 return True # All good.
139 LOG.debug("Waiting for %s to get to %s status. "
140 "Currently in %s status",
141 thing, log_status, new_status)
142 if not call_until_true(
143 check_status,
144 self.conf.build_timeout,
145 self.conf.build_interval):
146 message = ("Timed out waiting for thing %s "
147 "to become %s") % (thing_id, log_status)
148 raise exceptions.TimeoutException(message)
149
150 def get_remote_client(self, server_or_ip, username, private_key=None):
151 if isinstance(server_or_ip, six.string_types):
152 ip = server_or_ip
153 else:
154 network_name_for_ssh = self.conf.network_for_ssh
155 ip = server_or_ip.networks[network_name_for_ssh][0]
156 if private_key is None:
157 private_key = self.keypair.private_key
158 linux_client = remote_client.RemoteClient(ip, username,
159 pkey=private_key,
160 conf=self.conf)
161 try:
162 linux_client.validate_authentication()
163 except exceptions.SSHTimeout:
164 LOG.exception('ssh connection to %s failed' % ip)
165 raise
166
167 return linux_client
168
169 def _log_console_output(self, servers=None):
170 if not servers:
171 servers = self.compute_client.servers.list()
172 for server in servers:
173 LOG.debug('Console output for %s', server.id)
174 LOG.debug(server.get_console_output())
175
176 def _load_template(self, base_file, file_name):
177 filepath = os.path.join(os.path.dirname(os.path.realpath(base_file)),
178 file_name)
179 with open(filepath) as f:
180 return f.read()
181
182 def create_keypair(self, client=None, name=None):
183 if client is None:
184 client = self.compute_client
185 if name is None:
186 name = rand_name('heat-keypair')
187 keypair = client.keypairs.create(name)
188 self.assertEqual(keypair.name, name)
189
190 def delete_keypair():
191 keypair.delete()
192
193 self.addCleanup(delete_keypair)
194 return keypair
195
196 @classmethod
197 def _stack_rand_name(cls):
198 return rand_name(cls.__name__)
199
200 def _get_default_network(self):
201 networks = self.network_client.list_networks()
202 for net in networks['networks']:
203 if net['name'] == self.conf.fixed_network_name:
204 return net
205
206 @staticmethod
207 def _stack_output(stack, output_key):
208 """Return a stack output value for a given key."""
209 return next((o['output_value'] for o in stack.outputs
210 if o['output_key'] == output_key), None)
211
212 def _ping_ip_address(self, ip_address, should_succeed=True):
213 cmd = ['ping', '-c1', '-w1', ip_address]
214
215 def ping():
216 proc = subprocess.Popen(cmd,
217 stdout=subprocess.PIPE,
218 stderr=subprocess.PIPE)
219 proc.wait()
220 return (proc.returncode == 0) == should_succeed
221
222 return call_until_true(
223 ping, self.conf.build_timeout, 1)
224
225 def _wait_for_resource_status(self, stack_identifier, resource_name,
226 status, failure_pattern='^.*_FAILED$',
227 success_on_not_found=False):
228 """Waits for a Resource to reach a given status."""
229 fail_regexp = re.compile(failure_pattern)
230 build_timeout = self.conf.build_timeout
231 build_interval = self.conf.build_interval
232
233 start = timeutils.utcnow()
234 while timeutils.delta_seconds(start,
235 timeutils.utcnow()) < build_timeout:
236 try:
237 res = self.client.resources.get(
238 stack_identifier, resource_name)
239 except heat_exceptions.HTTPNotFound:
240 if success_on_not_found:
241 return
242 # ignore this, as the resource may not have
243 # been created yet
244 else:
245 if res.resource_status == status:
246 return
247 if fail_regexp.search(res.resource_status):
248 raise exceptions.StackResourceBuildErrorException(
249 resource_name=res.resource_name,
250 stack_identifier=stack_identifier,
251 resource_status=res.resource_status,
252 resource_status_reason=res.resource_status_reason)
253 time.sleep(build_interval)
254
255 message = ('Resource %s failed to reach %s status within '
256 'the required time (%s s).' %
257 (res.resource_name, status, build_timeout))
258 raise exceptions.TimeoutException(message)
259
260 def _wait_for_stack_status(self, stack_identifier, status,
261 failure_pattern='^.*_FAILED$',
262 success_on_not_found=False):
263 """
264 Waits for a Stack to reach a given status.
265
266 Note this compares the full $action_$status, e.g
267 CREATE_COMPLETE, not just COMPLETE which is exposed
268 via the status property of Stack in heatclient
269 """
270 fail_regexp = re.compile(failure_pattern)
271 build_timeout = self.conf.build_timeout
272 build_interval = self.conf.build_interval
273
274 start = timeutils.utcnow()
275 while timeutils.delta_seconds(start,
276 timeutils.utcnow()) < build_timeout:
277 try:
278 stack = self.client.stacks.get(stack_identifier)
279 except heat_exceptions.HTTPNotFound:
280 if success_on_not_found:
281 return
282 # ignore this, as the resource may not have
283 # been created yet
284 else:
285 if stack.stack_status == status:
286 return
287 if fail_regexp.search(stack.stack_status):
288 raise exceptions.StackBuildErrorException(
289 stack_identifier=stack_identifier,
290 stack_status=stack.stack_status,
291 stack_status_reason=stack.stack_status_reason)
292 time.sleep(build_interval)
293
294 message = ('Stack %s failed to reach %s status within '
295 'the required time (%s s).' %
296 (stack.stack_name, status, build_timeout))
297 raise exceptions.TimeoutException(message)
298
299 def _stack_delete(self, stack_identifier):
300 try:
301 self.client.stacks.delete(stack_identifier)
302 except heat_exceptions.HTTPNotFound:
303 pass
304 self._wait_for_stack_status(
305 stack_identifier, 'DELETE_COMPLETE',
306 success_on_not_found=True)