blob: 5464c032b2ab82f557242880e60382569175deb7 [file] [log] [blame]
ZhiQiang Fan39f97222013-09-20 04:49:44 +08001# Copyright 2012 OpenStack Foundation
Jay Pipes051075a2012-04-28 17:39:37 -04002# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
Attila Fazekasf86fa312013-07-30 19:56:39 +020016import atexit
Masayuki Igawa80c1b9f2013-10-07 17:19:11 +090017import functools
Marc Koderer24eb89c2014-01-31 11:23:33 +010018import json
Ian Wienand98c35f32013-07-23 20:34:23 +100019import os
Jay Pipes051075a2012-04-28 17:39:37 -040020import time
Marc Koderer24eb89c2014-01-31 11:23:33 +010021import urllib
22import uuid
Jay Pipes051075a2012-04-28 17:39:37 -040023
Matthew Treinish78561ad2013-07-26 11:41:56 -040024import fixtures
Chris Yeoh55530bb2013-02-08 16:04:27 +103025import nose.plugins.attrib
Attila Fazekasdc216422013-01-29 15:12:14 +010026import testresources
ivan-zhu1feeb382013-01-24 10:14:39 +080027import testtools
Jay Pipes051075a2012-04-28 17:39:37 -040028
Matthew Treinish3e046852013-07-23 16:00:24 -040029from tempest import clients
Marc Koderer24eb89c2014-01-31 11:23:33 +010030from tempest.common import generate_json
Ryan Hsu6c4bb3d2013-10-21 21:22:50 -070031from tempest.common import isolated_creds
Attila Fazekasdc216422013-01-29 15:12:14 +010032from tempest import config
Matthew Treinish16c43792013-09-09 19:55:23 +000033from tempest import exceptions
Matthew Treinishf4a9b0f2013-07-26 16:58:26 -040034from tempest.openstack.common import log as logging
Jay Pipes051075a2012-04-28 17:39:37 -040035
36LOG = logging.getLogger(__name__)
37
Sean Dague86bd8422013-12-20 09:56:44 -050038CONF = config.CONF
39
Samuel Merritt0d499bc2013-06-19 12:08:23 -070040# All the successful HTTP status codes from RFC 2616
41HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206)
42
Jay Pipes051075a2012-04-28 17:39:37 -040043
Chris Yeoh55530bb2013-02-08 16:04:27 +103044def attr(*args, **kwargs):
45 """A decorator which applies the nose and testtools attr decorator
46
47 This decorator applies the nose attr decorator as well as the
48 the testtools.testcase.attr if it is in the list of attributes
Attila Fazekasb2902af2013-02-16 16:22:44 +010049 to testtools we want to apply.
50 """
Chris Yeoh55530bb2013-02-08 16:04:27 +103051
52 def decorator(f):
Giulio Fidente4946a052013-05-14 12:23:51 +020053 if 'type' in kwargs and isinstance(kwargs['type'], str):
54 f = testtools.testcase.attr(kwargs['type'])(f)
Chris Yeohcf3fb7c2013-05-19 15:59:00 +093055 if kwargs['type'] == 'smoke':
56 f = testtools.testcase.attr('gate')(f)
Giulio Fidente4946a052013-05-14 12:23:51 +020057 elif 'type' in kwargs and isinstance(kwargs['type'], list):
58 for attr in kwargs['type']:
59 f = testtools.testcase.attr(attr)(f)
Chris Yeohcf3fb7c2013-05-19 15:59:00 +093060 if attr == 'smoke':
61 f = testtools.testcase.attr('gate')(f)
Giulio Fidente4946a052013-05-14 12:23:51 +020062 return nose.plugins.attrib.attr(*args, **kwargs)(f)
Chris Yeoh55530bb2013-02-08 16:04:27 +103063
64 return decorator
65
66
Matthew Treinish16c43792013-09-09 19:55:23 +000067def services(*args, **kwargs):
68 """A decorator used to set an attr for each service used in a test case
69
70 This decorator applies a testtools attr for each service that gets
71 exercised by a test case.
72 """
73 valid_service_list = ['compute', 'image', 'volume', 'orchestration',
Matthew Treinish03c4f772014-02-02 13:37:44 -050074 'network', 'identity', 'object_storage', 'dashboard']
Matthew Treinish16c43792013-09-09 19:55:23 +000075
76 def decorator(f):
77 for service in args:
78 if service not in valid_service_list:
79 raise exceptions.InvalidServiceTag('%s is not a valid service'
80 % service)
81 attr(type=list(args))(f)
82 return f
83 return decorator
84
85
Marc Koderer32221b8e2013-08-23 13:57:50 +020086def stresstest(*args, **kwargs):
87 """Add stress test decorator
88
89 For all functions with this decorator a attr stress will be
90 set automatically.
91
92 @param class_setup_per: allowed values are application, process, action
93 ``application``: once in the stress job lifetime
94 ``process``: once in the worker process lifetime
95 ``action``: on each action
Marc Kodererb0604412013-09-02 09:43:40 +020096 @param allow_inheritance: allows inheritance of this attribute
Marc Koderer32221b8e2013-08-23 13:57:50 +020097 """
98 def decorator(f):
99 if 'class_setup_per' in kwargs:
100 setattr(f, "st_class_setup_per", kwargs['class_setup_per'])
101 else:
102 setattr(f, "st_class_setup_per", 'process')
Marc Kodererb0604412013-09-02 09:43:40 +0200103 if 'allow_inheritance' in kwargs:
104 setattr(f, "st_allow_inheritance", kwargs['allow_inheritance'])
105 else:
106 setattr(f, "st_allow_inheritance", False)
Marc Koderer32221b8e2013-08-23 13:57:50 +0200107 attr(type='stress')(f)
108 return f
109 return decorator
110
111
Giulio Fidente83181a92013-10-01 06:02:24 +0200112def skip_because(*args, **kwargs):
113 """A decorator useful to skip tests hitting known bugs
114
115 @param bug: bug number causing the test to skip
116 @param condition: optional condition to be True for the skip to have place
Ken'ichi Ohmichia1aa44c2013-12-06 20:48:24 +0900117 @param interface: skip the test if it is the same as self._interface
Giulio Fidente83181a92013-10-01 06:02:24 +0200118 """
119 def decorator(f):
Masayuki Igawa80c1b9f2013-10-07 17:19:11 +0900120 @functools.wraps(f)
Ken'ichi Ohmichia1aa44c2013-12-06 20:48:24 +0900121 def wrapper(self, *func_args, **func_kwargs):
122 skip = False
123 if "condition" in kwargs:
124 if kwargs["condition"] is True:
125 skip = True
126 elif "interface" in kwargs:
127 if kwargs["interface"] == self._interface:
128 skip = True
129 else:
130 skip = True
131 if "bug" in kwargs and skip is True:
132 msg = "Skipped until Bug: %s is resolved." % kwargs["bug"]
133 raise testtools.TestCase.skipException(msg)
134 return f(self, *func_args, **func_kwargs)
Masayuki Igawa80c1b9f2013-10-07 17:19:11 +0900135 return wrapper
Giulio Fidente83181a92013-10-01 06:02:24 +0200136 return decorator
137
138
Matthew Treinishe3d26142013-11-26 19:14:58 +0000139def requires_ext(*args, **kwargs):
140 """A decorator to skip tests if an extension is not enabled
141
142 @param extension
143 @param service
144 """
145 def decorator(func):
146 @functools.wraps(func)
147 def wrapper(*func_args, **func_kwargs):
148 if not is_extension_enabled(kwargs['extension'],
149 kwargs['service']):
150 msg = "Skipped because %s extension: %s is not enabled" % (
151 kwargs['service'], kwargs['extension'])
152 raise testtools.TestCase.skipException(msg)
153 return func(*func_args, **func_kwargs)
154 return wrapper
155 return decorator
156
157
158def is_extension_enabled(extension_name, service):
159 """A function that will check the list of enabled extensions from config
160
161 """
Matthew Treinishe3d26142013-11-26 19:14:58 +0000162 config_dict = {
Matthew Treinishbc0e03e2014-01-30 16:51:06 +0000163 'compute': CONF.compute_feature_enabled.api_extensions,
164 'compute_v3': CONF.compute_feature_enabled.api_v3_extensions,
165 'volume': CONF.volume_feature_enabled.api_extensions,
166 'network': CONF.network_feature_enabled.api_extensions,
167 'object': CONF.object_storage_feature_enabled.discoverable_apis,
Matthew Treinishe3d26142013-11-26 19:14:58 +0000168 }
169 if config_dict[service][0] == 'all':
170 return True
171 if extension_name in config_dict[service]:
172 return True
173 return False
174
Ian Wienand98c35f32013-07-23 20:34:23 +1000175# there is a mis-match between nose and testtools for older pythons.
176# testtools will set skipException to be either
177# unittest.case.SkipTest, unittest2.case.SkipTest or an internal skip
178# exception, depending on what it can find. Python <2.7 doesn't have
179# unittest.case.SkipTest; so if unittest2 is not installed it falls
180# back to the internal class.
181#
182# The current nose skip plugin will decide to raise either
183# unittest.case.SkipTest or its own internal exception; it does not
184# look for unittest2 or the internal unittest exception. Thus we must
185# monkey-patch testtools.TestCase.skipException to be the exception
186# the nose skip plugin expects.
187#
188# However, with the switch to testr nose may not be available, so we
189# require you to opt-in to this fix with an environment variable.
190#
191# This is temporary until upstream nose starts looking for unittest2
192# as testtools does; we can then remove this and ensure unittest2 is
193# available for older pythons; then nose and testtools will agree
194# unittest2.case.SkipTest is the one-true skip test exception.
195#
196# https://review.openstack.org/#/c/33056
197# https://github.com/nose-devs/nose/pull/699
198if 'TEMPEST_PY26_NOSE_COMPAT' in os.environ:
199 try:
200 import unittest.case.SkipTest
201 # convince pep8 we're using the import...
202 if unittest.case.SkipTest:
203 pass
204 raise RuntimeError("You have unittest.case.SkipTest; "
205 "no need to override")
206 except ImportError:
207 LOG.info("Overriding skipException to nose SkipTest")
208 testtools.TestCase.skipException = nose.plugins.skip.SkipTest
209
Attila Fazekasf86fa312013-07-30 19:56:39 +0200210at_exit_set = set()
211
212
213def validate_tearDownClass():
214 if at_exit_set:
Vladislav Kuzmin0df88bb2014-01-28 14:58:48 +0400215 raise RuntimeError("tearDownClass does not call the super's "
Attila Fazekasf86fa312013-07-30 19:56:39 +0200216 "tearDownClass in these classes: "
Attila Fazekasd5d43b82013-10-09 16:02:19 +0200217 + str(at_exit_set) + "\n"
218 "If you see the exception, with another "
Vladislav Kuzmin0df88bb2014-01-28 14:58:48 +0400219 "exception please do not report this one! "
220 "If you are changing tempest code, make sure you "
Attila Fazekasd5d43b82013-10-09 16:02:19 +0200221 "are calling the super class's tearDownClass!")
Attila Fazekasf86fa312013-07-30 19:56:39 +0200222
223atexit.register(validate_tearDownClass)
224
Ian Wienand98c35f32013-07-23 20:34:23 +1000225
Attila Fazekasdc216422013-01-29 15:12:14 +0100226class BaseTestCase(testtools.TestCase,
227 testtools.testcase.WithAttributes,
228 testresources.ResourcedTestCase):
Attila Fazekasc43fec82013-04-09 23:17:52 +0200229
Attila Fazekasf86fa312013-07-30 19:56:39 +0200230 setUpClassCalled = False
Marc Koderer24eb89c2014-01-31 11:23:33 +0100231 _service = None
Attila Fazekasf86fa312013-07-30 19:56:39 +0200232
Matthew Treinish9f756a02014-01-15 10:26:07 -0500233 network_resources = {}
234
Pavel Sedlák1053bd32013-04-16 16:47:40 +0200235 @classmethod
236 def setUpClass(cls):
237 if hasattr(super(BaseTestCase, cls), 'setUpClass'):
238 super(BaseTestCase, cls).setUpClass()
Attila Fazekasf86fa312013-07-30 19:56:39 +0200239 cls.setUpClassCalled = True
Pavel Sedlák1053bd32013-04-16 16:47:40 +0200240
Attila Fazekasf86fa312013-07-30 19:56:39 +0200241 @classmethod
242 def tearDownClass(cls):
Attila Fazekas5d275302013-08-29 12:35:12 +0200243 at_exit_set.discard(cls)
Attila Fazekasf86fa312013-07-30 19:56:39 +0200244 if hasattr(super(BaseTestCase, cls), 'tearDownClass'):
245 super(BaseTestCase, cls).tearDownClass()
246
247 def setUp(self):
248 super(BaseTestCase, self).setUp()
249 if not self.setUpClassCalled:
250 raise RuntimeError("setUpClass does not calls the super's"
251 "setUpClass in the "
252 + self.__class__.__name__)
253 at_exit_set.add(self.__class__)
Matthew Treinish78561ad2013-07-26 11:41:56 -0400254 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
255 try:
256 test_timeout = int(test_timeout)
257 except ValueError:
258 test_timeout = 0
259 if test_timeout > 0:
Attila Fazekasf86fa312013-07-30 19:56:39 +0200260 self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
Matthew Treinish78561ad2013-07-26 11:41:56 -0400261
262 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
263 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
Attila Fazekasf86fa312013-07-30 19:56:39 +0200264 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
265 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
Matthew Treinish78561ad2013-07-26 11:41:56 -0400266 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
267 os.environ.get('OS_STDERR_CAPTURE') == '1'):
Attila Fazekasf86fa312013-07-30 19:56:39 +0200268 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
269 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
Attila Fazekas31388072013-08-15 08:58:07 +0200270 if (os.environ.get('OS_LOG_CAPTURE') != 'False' and
271 os.environ.get('OS_LOG_CAPTURE') != '0'):
272 log_format = '%(asctime)-15s %(message)s'
273 self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
Attila Fazekas90445be2013-10-24 16:46:03 +0200274 format=log_format,
275 level=None))
Matthew Treinish78561ad2013-07-26 11:41:56 -0400276
Matthew Treinish3e046852013-07-23 16:00:24 -0400277 @classmethod
Matthew Treinish8004e8c2014-01-27 23:03:14 +0000278 def get_client_manager(cls, interface=None):
Ryan Hsu6c4bb3d2013-10-21 21:22:50 -0700279 """
280 Returns an Openstack client manager
281 """
Matthew Treinish9f756a02014-01-15 10:26:07 -0500282 cls.isolated_creds = isolated_creds.IsolatedCreds(
283 cls.__name__, network_resources=cls.network_resources)
Ryan Hsu6c4bb3d2013-10-21 21:22:50 -0700284
285 force_tenant_isolation = getattr(cls, 'force_tenant_isolation', None)
Matthew Treinishbc0e03e2014-01-30 16:51:06 +0000286 if (CONF.compute.allow_tenant_isolation or
Ryan Hsu6c4bb3d2013-10-21 21:22:50 -0700287 force_tenant_isolation):
288 creds = cls.isolated_creds.get_primary_creds()
289 username, tenant_name, password = creds
Matthew Treinish8004e8c2014-01-27 23:03:14 +0000290 if getattr(cls, '_interface', None):
291 os = clients.Manager(username=username,
292 password=password,
293 tenant_name=tenant_name,
Marc Koderer24eb89c2014-01-31 11:23:33 +0100294 interface=cls._interface,
295 service=cls._service)
Matthew Treinish8004e8c2014-01-27 23:03:14 +0000296 elif interface:
297 os = clients.Manager(username=username,
298 password=password,
299 tenant_name=tenant_name,
Marc Koderer24eb89c2014-01-31 11:23:33 +0100300 interface=interface,
301 service=cls._service)
Matthew Treinish8004e8c2014-01-27 23:03:14 +0000302 else:
303 os = clients.Manager(username=username,
304 password=password,
Marc Koderer24eb89c2014-01-31 11:23:33 +0100305 tenant_name=tenant_name,
306 service=cls._service)
Ryan Hsu6c4bb3d2013-10-21 21:22:50 -0700307 else:
Matthew Treinish8004e8c2014-01-27 23:03:14 +0000308 if getattr(cls, '_interface', None):
Marc Koderer24eb89c2014-01-31 11:23:33 +0100309 os = clients.Manager(interface=cls._interface,
310 service=cls._service)
Matthew Treinish8004e8c2014-01-27 23:03:14 +0000311 elif interface:
Marc Koderer24eb89c2014-01-31 11:23:33 +0100312 os = clients.Manager(interface=interface, service=cls._service)
Matthew Treinish8004e8c2014-01-27 23:03:14 +0000313 else:
Marc Koderer24eb89c2014-01-31 11:23:33 +0100314 os = clients.Manager(service=cls._service)
Ryan Hsu6c4bb3d2013-10-21 21:22:50 -0700315 return os
316
317 @classmethod
318 def clear_isolated_creds(cls):
319 """
320 Clears isolated creds if set
321 """
322 if getattr(cls, 'isolated_creds'):
323 cls.isolated_creds.clear_isolated_creds()
324
325 @classmethod
Matthew Treinish3e046852013-07-23 16:00:24 -0400326 def _get_identity_admin_client(cls):
327 """
328 Returns an instance of the Identity Admin API client
329 """
Marc Koderer24eb89c2014-01-31 11:23:33 +0100330 os = clients.AdminManager(interface=cls._interface,
331 service=cls._service)
Matthew Treinish3e046852013-07-23 16:00:24 -0400332 admin_client = os.identity_client
333 return admin_client
334
335 @classmethod
Matthew Treinish9f756a02014-01-15 10:26:07 -0500336 def set_network_resources(self, network=False, router=False, subnet=False,
337 dhcp=False):
338 """Specify which network resources should be created
339
340 @param network
341 @param router
342 @param subnet
343 @param dhcp
344 """
Salvatore Orlando5a337242014-01-15 22:49:22 +0000345 # network resources should be set only once from callers
346 # in order to ensure that even if it's called multiple times in
347 # a chain of overloaded methods, the attribute is set only
348 # in the leaf class
349 if not self.network_resources:
350 self.network_resources = {
351 'network': network,
352 'router': router,
353 'subnet': subnet,
354 'dhcp': dhcp}
Matthew Treinish9f756a02014-01-15 10:26:07 -0500355
Attila Fazekasdc216422013-01-29 15:12:14 +0100356
Marc Koderer24eb89c2014-01-31 11:23:33 +0100357class NegativeAutoTest(BaseTestCase):
358
359 _resources = {}
360
361 @classmethod
362 def setUpClass(cls):
363 super(NegativeAutoTest, cls).setUpClass()
364 os = cls.get_client_manager()
365 cls.client = os.negative_client
366
367 @staticmethod
368 def load_schema(file):
369 """
370 Loads a schema from a file on a specified location.
371
372 :param file: the file name
373 """
374 #NOTE(mkoderer): must be extended for xml support
375 fn = os.path.join(
376 os.path.abspath(os.path.dirname(os.path.dirname(__file__))),
377 "etc", "schemas", file)
378 LOG.debug("Open schema file: %s" % (fn))
379 return json.load(open(fn))
380
381 @staticmethod
382 def generate_scenario(description_file):
383 """
384 Generates the test scenario list for a given description.
385
386 :param description: A dictionary with the following entries:
387 name (required) name for the api
388 http-method (required) one of HEAD,GET,PUT,POST,PATCH,DELETE
389 url (required) the url to be appended to the catalog url with '%s'
390 for each resource mentioned
391 resources: (optional) A list of resource names such as "server",
392 "flavor", etc. with an element for each '%s' in the url. This
393 method will call self.get_resource for each element when
394 constructing the positive test case template so negative
395 subclasses are expected to return valid resource ids when
396 appropriate.
397 json-schema (optional) A valid json schema that will be used to
398 create invalid data for the api calls. For "GET" and "HEAD",
399 the data is used to generate query strings appended to the url,
400 otherwise for the body of the http call.
401 """
402 description = NegativeAutoTest.load_schema(description_file)
403 LOG.debug(description)
404 generate_json.validate_negative_test_schema(description)
405 schema = description.get("json-schema", None)
406 resources = description.get("resources", [])
407 scenario_list = []
408 for resource in resources:
409 LOG.debug("Add resource to test %s" % resource)
410 scn_name = "inv_res_%s" % (resource)
411 scenario_list.append((scn_name, {"resource": (resource,
412 str(uuid.uuid4()))
413 }))
414 if schema is not None:
415 for invalid in generate_json.generate_invalid(schema):
416 scenario_list.append((invalid[0],
417 {"schema": invalid[1],
418 "expected_result": invalid[2]}))
419 LOG.debug(scenario_list)
420 return scenario_list
421
422 def execute(self, description_file):
423 """
424 Execute a http call on an api that are expected to
425 result in client errors. First it uses invalid resources that are part
426 of the url, and then invalid data for queries and http request bodies.
427
428 :param description: A dictionary with the following entries:
429 name (required) name for the api
430 http-method (required) one of HEAD,GET,PUT,POST,PATCH,DELETE
431 url (required) the url to be appended to the catalog url with '%s'
432 for each resource mentioned
433 resources: (optional) A list of resource names such as "server",
434 "flavor", etc. with an element for each '%s' in the url. This
435 method will call self.get_resource for each element when
436 constructing the positive test case template so negative
437 subclasses are expected to return valid resource ids when
438 appropriate.
439 json-schema (optional) A valid json schema that will be used to
440 create invalid data for the api calls. For "GET" and "HEAD",
441 the data is used to generate query strings appended to the url,
442 otherwise for the body of the http call.
443
444 """
445 description = NegativeAutoTest.load_schema(description_file)
446 LOG.info("Executing %s" % description["name"])
447 LOG.debug(description)
448 method = description["http-method"]
449 url = description["url"]
450
451 resources = [self.get_resource(r) for
452 r in description.get("resources", [])]
453
454 if hasattr(self, "resource"):
455 # Note(mkoderer): The resources list already contains an invalid
456 # entry (see get_resource).
457 # We just send a valid json-schema with it
458 valid = None
459 schema = description.get("json-schema", None)
460 if schema:
461 valid = generate_json.generate_valid(schema)
462 new_url, body = self._http_arguments(valid, url, method)
463 resp, resp_body = self.client.send_request(method, new_url,
464 resources, body=body)
465 self._check_negative_response(resp.status, resp_body)
466 return
467
468 if hasattr(self, "schema"):
469 new_url, body = self._http_arguments(self.schema, url, method)
470 resp, resp_body = self.client.send_request(method, new_url,
471 resources, body=body)
472 self._check_negative_response(resp.status, resp_body)
473
474 def _http_arguments(self, json_dict, url, method):
475 LOG.debug("dict: %s url: %s method: %s" % (json_dict, url, method))
476 if not json_dict:
477 return url, None
478 elif method in ["GET", "HEAD", "PUT", "DELETE"]:
479 return "%s?%s" % (url, urllib.urlencode(json_dict)), None
480 else:
481 return url, json.dumps(json_dict)
482
483 def _check_negative_response(self, result, body):
484 expected_result = getattr(self, "expected_result", None)
485 self.assertTrue(result >= 400 and result < 500 and result != 413,
486 "Expected client error, got %s:%s" %
487 (result, body))
488 self.assertTrue(expected_result is None or expected_result == result,
489 "Expected %s, got %s:%s" %
490 (expected_result, result, body))
491
492 @classmethod
493 def set_resource(cls, name, resource):
494 """
495 This function can be used in setUpClass context to register a resoruce
496 for a test.
497
498 :param name: The name of the kind of resource such as "flavor", "role",
499 etc.
500 :resource: The id of the resource
501 """
502 cls._resources[name] = resource
503
504 def get_resource(self, name):
505 """
506 Return a valid uuid for a type of resource. If a real resource is
507 needed as part of a url then this method should return one. Otherwise
508 it can return None.
509
510 :param name: The name of the kind of resource such as "flavor", "role",
511 etc.
512 """
513 if hasattr(self, "resource") and self.resource[0] == name:
514 LOG.debug("Return invalid resource (%s) value: %s" %
515 (self.resource[0], self.resource[1]))
516 return self.resource[1]
517 if name in self._resources:
518 return self._resources[name]
519 return None
520
521
Sean Dague35a7caf2013-05-10 10:38:22 -0400522def call_until_true(func, duration, sleep_for):
523 """
524 Call the given function until it returns True (and return True) or
525 until the specified duration (in seconds) elapses (and return
526 False).
527
528 :param func: A zero argument callable that returns True on success.
529 :param duration: The number of seconds for which to attempt a
530 successful call of the function.
531 :param sleep_for: The number of seconds to sleep after an unsuccessful
532 invocation of the function.
533 """
534 now = time.time()
535 timeout = now + duration
536 while now < timeout:
537 if func():
538 return True
539 LOG.debug("Sleeping for %d seconds", sleep_for)
540 time.sleep(sleep_for)
541 now = time.time()
542 return False