3a64909311
This is to add missing ws separator between words. Change-Id: I4e49d6bfa4293751273d5ec95fcf60478b96792d
471 lines
17 KiB
Python
471 lines
17 KiB
Python
# Copyright 2013: Mirantis Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import collections
|
|
import itertools
|
|
import time
|
|
import traceback
|
|
|
|
import jsonschema
|
|
import six
|
|
|
|
from rally.common import logging
|
|
from rally import consts
|
|
from rally import exceptions
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def get_status(resource, status_attr="status"):
|
|
"""Get the status of a given resource object.
|
|
|
|
The status is returned in upper case. The status is checked for the
|
|
standard field names with special cases for Heat and Ceilometer.
|
|
|
|
:param resource: The resource object or dict.
|
|
:param status_attr: Allows to specify non-standard status fields.
|
|
:return: The status or "NONE" if it is not available.
|
|
"""
|
|
|
|
for s_attr in ["stack_status", "state", status_attr]:
|
|
status = getattr(resource, s_attr, None)
|
|
if isinstance(status, six.string_types):
|
|
return status.upper()
|
|
|
|
# Dict case
|
|
if ((isinstance(resource, dict) and status_attr in resource.keys() and
|
|
isinstance(resource[status_attr], six.string_types))):
|
|
return resource[status_attr].upper()
|
|
|
|
return "NONE"
|
|
|
|
|
|
class resource_is(object):
|
|
def __init__(self, desired_status, status_getter=None):
|
|
self.desired_status = desired_status
|
|
self.status_getter = status_getter or get_status
|
|
|
|
def __call__(self, resource):
|
|
return self.status_getter(resource) == self.desired_status.upper()
|
|
|
|
def __str__(self):
|
|
return str(self.desired_status)
|
|
|
|
|
|
def get_from_manager(error_statuses=None):
|
|
error_statuses = error_statuses or ["ERROR"]
|
|
error_statuses = map(lambda str: str.upper(), error_statuses)
|
|
|
|
def _get_from_manager(resource, id_attr="id"):
|
|
# catch client side errors
|
|
try:
|
|
res = resource.manager.get(getattr(resource, id_attr))
|
|
except Exception as e:
|
|
if getattr(e, "code", getattr(e, "http_status", 400)) == 404:
|
|
raise exceptions.GetResourceNotFound(resource=resource)
|
|
raise exceptions.GetResourceFailure(resource=resource, err=e)
|
|
|
|
# catch abnormal status, such as "no valid host" for servers
|
|
status = get_status(res)
|
|
|
|
if status in ("DELETED", "DELETE_COMPLETE"):
|
|
raise exceptions.GetResourceNotFound(resource=res)
|
|
if status in error_statuses:
|
|
raise exceptions.GetResourceErrorStatus(
|
|
resource=res, status=status,
|
|
fault=getattr(res, "fault", "n/a"))
|
|
|
|
return res
|
|
|
|
return _get_from_manager
|
|
|
|
|
|
def manager_list_size(sizes):
|
|
def _list(mgr):
|
|
return len(mgr.list()) in sizes
|
|
return _list
|
|
|
|
|
|
@logging.log_deprecated("Use wait_for_status instead.", "0.1.2", once=True)
|
|
def wait_for(resource, is_ready=None, ready_statuses=None,
|
|
failure_statuses=None, status_attr="status", update_resource=None,
|
|
timeout=60, check_interval=1, id_attr="id"):
|
|
"""Waits for the given resource to come into the one of the given statuses.
|
|
|
|
The method can be used to check resource for status with a `is_ready`
|
|
function or with a list of expected statuses and the status attribute
|
|
|
|
In case when the is_ready checker is not provided the resource should have
|
|
status_attr. It may be an object attribute or a dictionary key. The value
|
|
of the attribute is checked against ready statuses list and failure
|
|
statuses. In case of a failure the wait exits with an exception. The
|
|
resource is updated between iterations with an update_resource call.
|
|
|
|
:param is_ready: A predicate that should take the resource object and
|
|
return True iff it is ready to be returned
|
|
:param ready_statuses: List of statuses which mean that the resource is
|
|
ready
|
|
:param failure_statuses: List of statuses which mean that an error has
|
|
occurred while waiting for the resource
|
|
:param status_attr: The name of the status attribute of the resource
|
|
:param update_resource: Function that should take the resource object
|
|
and return an 'updated' resource. If set to
|
|
None, no result updating is performed
|
|
:param timeout: Timeout in seconds after which a TimeoutException will be
|
|
raised
|
|
:param check_interval: Interval in seconds between the two consecutive
|
|
readiness checks
|
|
|
|
:returns: The "ready" resource object
|
|
"""
|
|
|
|
if is_ready is not None:
|
|
return wait_is_ready(resource=resource, is_ready=is_ready,
|
|
update_resource=update_resource, timeout=timeout,
|
|
check_interval=check_interval)
|
|
else:
|
|
return wait_for_status(resource=resource,
|
|
ready_statuses=ready_statuses,
|
|
failure_statuses=failure_statuses,
|
|
status_attr=status_attr,
|
|
update_resource=update_resource,
|
|
timeout=timeout,
|
|
check_interval=check_interval,
|
|
id_attr=id_attr)
|
|
|
|
|
|
@logging.log_deprecated("Use wait_for_status instead.", "0.1.2", once=True)
|
|
def wait_is_ready(resource, is_ready, update_resource=None,
|
|
timeout=60, check_interval=1):
|
|
|
|
resource_repr = getattr(resource, "name", repr(resource))
|
|
start = time.time()
|
|
|
|
while True:
|
|
if update_resource is not None:
|
|
resource = update_resource(resource)
|
|
|
|
if is_ready(resource):
|
|
return resource
|
|
|
|
time.sleep(check_interval)
|
|
if time.time() - start > timeout:
|
|
raise exceptions.TimeoutException(
|
|
desired_status=str(is_ready),
|
|
resource_name=resource_repr,
|
|
resource_type=resource.__class__.__name__,
|
|
resource_id=getattr(resource, "id", "<no id>"),
|
|
resource_status=get_status(resource),
|
|
timeout=timeout)
|
|
|
|
|
|
def wait_for_status(resource, ready_statuses, failure_statuses=["error"],
|
|
status_attr="status", update_resource=None,
|
|
timeout=60, check_interval=1, check_deletion=False,
|
|
id_attr="id"):
|
|
|
|
resource_repr = getattr(resource, "name", repr(resource))
|
|
if not isinstance(ready_statuses, (set, list, tuple)):
|
|
raise ValueError("Ready statuses should be supplied as set, list or "
|
|
"tuple")
|
|
if failure_statuses and not isinstance(failure_statuses,
|
|
(set, list, tuple)):
|
|
raise ValueError("Failure statuses should be supplied as set, list or "
|
|
"tuple")
|
|
|
|
# make all statuses upper case
|
|
ready_statuses = set(s.upper() for s in ready_statuses or [])
|
|
failure_statuses = set(s.upper() for s in failure_statuses or [])
|
|
|
|
if (ready_statuses & failure_statuses):
|
|
raise ValueError(
|
|
"Can't wait for resource's %s status. Ready and Failure "
|
|
"statuses conflict." % resource_repr)
|
|
if not ready_statuses:
|
|
raise ValueError(
|
|
"Can't wait for resource's %s status. No ready "
|
|
"statuses provided" % resource_repr)
|
|
if not update_resource:
|
|
raise ValueError(
|
|
"Can't wait for resource's %s status. No update method."
|
|
% resource_repr)
|
|
|
|
start = time.time()
|
|
|
|
latest_status = get_status(resource, status_attr)
|
|
latest_status_update = start
|
|
|
|
while True:
|
|
try:
|
|
if id_attr == "id":
|
|
resource = update_resource(resource)
|
|
else:
|
|
resource = update_resource(resource, id_attr=id_attr)
|
|
except exceptions.GetResourceNotFound:
|
|
if check_deletion:
|
|
return
|
|
else:
|
|
raise
|
|
status = get_status(resource, status_attr)
|
|
|
|
if status != latest_status:
|
|
current_time = time.time()
|
|
delta = current_time - latest_status_update
|
|
LOG.debug(
|
|
"Waiting for resource %(resource)s. Status changed: "
|
|
"%(latest)s => %(current)s in %(delta)s"
|
|
% {"resource": resource_repr, "latest": latest_status,
|
|
"current": status, "delta": delta})
|
|
|
|
latest_status = status
|
|
latest_status_update = current_time
|
|
|
|
if status in ready_statuses:
|
|
return resource
|
|
if status in failure_statuses:
|
|
raise exceptions.GetResourceErrorStatus(
|
|
resource=resource,
|
|
status=status,
|
|
fault="Status in failure list %s" % str(failure_statuses))
|
|
|
|
time.sleep(check_interval)
|
|
if time.time() - start > timeout:
|
|
raise exceptions.TimeoutException(
|
|
desired_status="('%s')" % "', '".join(ready_statuses),
|
|
resource_name=resource_repr,
|
|
resource_type=resource.__class__.__name__,
|
|
resource_id=getattr(resource, id_attr, "<no id>"),
|
|
resource_status=get_status(resource, status_attr),
|
|
timeout=timeout)
|
|
|
|
|
|
@logging.log_deprecated("Use wait_for_status instead.", "0.1.2", once=True)
|
|
def wait_for_delete(resource, update_resource=None, timeout=60,
|
|
check_interval=1):
|
|
"""Wait for the full deletion of resource.
|
|
|
|
:param update_resource: Function that should take the resource object
|
|
and return an 'updated' resource, or raise
|
|
exception rally.exceptions.GetResourceNotFound
|
|
that means that resource is deleted.
|
|
|
|
:param timeout: Timeout in seconds after which a TimeoutException will be
|
|
raised
|
|
:param check_interval: Interval in seconds between the two consecutive
|
|
readiness checks
|
|
"""
|
|
start = time.time()
|
|
while True:
|
|
try:
|
|
resource = update_resource(resource)
|
|
except exceptions.GetResourceNotFound:
|
|
break
|
|
time.sleep(check_interval)
|
|
if time.time() - start > timeout:
|
|
raise exceptions.TimeoutException(
|
|
desired_status="deleted",
|
|
resource_name=getattr(resource, "name", repr(resource)),
|
|
resource_type=resource.__class__.__name__,
|
|
resource_id=getattr(resource, "id", "<no id>"),
|
|
resource_status=get_status(resource),
|
|
timeout=timeout)
|
|
|
|
|
|
def format_exc(exc):
|
|
return [exc.__class__.__name__, str(exc), traceback.format_exc()]
|
|
|
|
|
|
def infinite_run_args_generator(args_func):
|
|
for i in itertools.count():
|
|
try:
|
|
yield args_func(i)
|
|
except StopIteration:
|
|
return
|
|
|
|
|
|
class ActionBuilder(object):
|
|
"""Builder class for mapping and creating action objects.
|
|
|
|
An action list is an array of single key/value dicts which takes
|
|
the form:
|
|
|
|
[{"action": times}, {"action": times}...]
|
|
|
|
Here 'action' is a string which indicates an action to perform and
|
|
'times' is a non-zero positive integer which specifies how many
|
|
times to run the action in sequence.
|
|
|
|
This utility builder class will build and return methods which
|
|
wrapper the action call the given amount of times.
|
|
"""
|
|
|
|
SCHEMA_TEMPLATE = {
|
|
"type": "array",
|
|
"$schema": consts.JSON_SCHEMA,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {},
|
|
"additionalProperties": False,
|
|
"minItems": 0
|
|
}
|
|
}
|
|
|
|
ITEM_TEMPLATE = {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"exclusiveMinimum": True,
|
|
"optional": True
|
|
}
|
|
|
|
def __init__(self, action_keywords):
|
|
"""Create a new instance of the builder for the given action keywords.
|
|
|
|
:param action_keywords: A list of strings which are the keywords this
|
|
instance of the builder supports.
|
|
"""
|
|
self._bindings = {}
|
|
self.schema = dict(ActionBuilder.SCHEMA_TEMPLATE)
|
|
for kw in action_keywords:
|
|
self.schema["items"]["properties"][kw] = (
|
|
ActionBuilder.ITEM_TEMPLATE)
|
|
|
|
def bind_action(self, action_key, action, *args, **kwargs):
|
|
"""Bind an action to an action key.
|
|
|
|
Static args/kwargs can be optionally binded.
|
|
:param action_key: The action keyword to bind the action to.
|
|
:param action: A method/function to call for the action.
|
|
:param args: (optional) Static positional args to prepend
|
|
to all invocations of the action.
|
|
:param kwargs: (optional) Static kwargs to prepend to all
|
|
invocations of the action.
|
|
"""
|
|
self.validate([{action_key: 1}])
|
|
self._bindings[action_key] = {
|
|
"action": action,
|
|
"args": args or (),
|
|
"kwargs": kwargs or {}
|
|
}
|
|
|
|
def validate(self, actions):
|
|
"""Validate the list of action objects against the builder schema.
|
|
|
|
:param actions: The list of action objects to validate.
|
|
"""
|
|
jsonschema.validate(actions, self.schema)
|
|
|
|
def _build(self, func, times, *args, **kwargs):
|
|
"""Build the wrapper action call."""
|
|
def _f():
|
|
for i in range(times):
|
|
func(*args, **kwargs)
|
|
return _f
|
|
|
|
def build_actions(self, actions, *args, **kwargs):
|
|
"""Build a list of callable actions.
|
|
|
|
A list of callable actions based on the given action object list and
|
|
the actions bound to this builder.
|
|
|
|
:param actions: A list of action objects to build callable
|
|
action for.
|
|
:param args: (optional) Positional args to pass into each
|
|
built action. These will be appended to any args set for the
|
|
action via its binding.
|
|
:param kwargs: (optional) Keyword args to pass into each built
|
|
action. These will be appended to any kwargs set for the action
|
|
via its binding.
|
|
"""
|
|
self.validate(actions)
|
|
bound_actions = []
|
|
for action in actions:
|
|
action_key = list(action)[0]
|
|
times = action.get(action_key)
|
|
binding = self._bindings.get(action_key)
|
|
dft_kwargs = dict(binding["kwargs"])
|
|
dft_kwargs.update(kwargs or {})
|
|
bound_actions.append(
|
|
self._build(binding["action"], times,
|
|
*(binding["args"] + args), **dft_kwargs))
|
|
return bound_actions
|
|
|
|
|
|
# TODO(andreykurilin): We need to implement some wrapper for atomic actions,
|
|
# we can use these wrapper to simulate new and old format.
|
|
class WrapperForAtomicActions(list):
|
|
|
|
def __init__(self, atomic_actions, timestamp=0):
|
|
|
|
self.timestamp = timestamp
|
|
|
|
if isinstance(atomic_actions, list):
|
|
self.__atomic_actions = atomic_actions
|
|
self.__old_atomic_actions = self._convert_new_atomic_actions(
|
|
self.__atomic_actions)
|
|
else:
|
|
self.__atomic_actions = self._convert_old_atomic_actions(
|
|
atomic_actions)
|
|
self.__old_atomic_actions = atomic_actions
|
|
|
|
super(WrapperForAtomicActions, self).__init__(self.__atomic_actions)
|
|
|
|
def _convert_old_atomic_actions(self, old_atomic_actions):
|
|
"""Convert atomic actions to new format. """
|
|
atomic_actions = []
|
|
started_at = self.timestamp
|
|
for name, duration in old_atomic_actions.items():
|
|
finished_at = started_at + duration
|
|
atomic_actions.append({"name": name,
|
|
"started_at": started_at,
|
|
"finished_at": finished_at,
|
|
"children": []})
|
|
started_at = finished_at
|
|
return atomic_actions
|
|
|
|
def _convert_new_atomic_actions(self, atomic_actions):
|
|
"""Convert atomic actions to old format. """
|
|
old_style = collections.OrderedDict()
|
|
for action in atomic_actions:
|
|
duration = action["finished_at"] - action["started_at"]
|
|
if action["name"] in old_style:
|
|
name_template = action["name"] + " (%i)"
|
|
i = 2
|
|
while name_template % i in old_style:
|
|
i += 1
|
|
old_style[name_template % i] = duration
|
|
else:
|
|
old_style[action["name"]] = duration
|
|
return old_style
|
|
|
|
def items(self):
|
|
return self.__old_atomic_actions.items()
|
|
|
|
def get(self, name, default=None):
|
|
return self.__old_atomic_actions.get(name, default)
|
|
|
|
def __iter__(self):
|
|
return iter(self.__atomic_actions)
|
|
|
|
def __len__(self):
|
|
return len(self.__atomic_actions)
|
|
|
|
def __getitem__(self, item):
|
|
if isinstance(item, int):
|
|
# it is a call to list:
|
|
return self.__atomic_actions[item]
|
|
else:
|
|
return self.__old_atomic_actions[item]
|