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