# 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", ""), 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, ""), 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", ""), 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_SCHEMA7, "items": { "type": "object", "properties": {}, "additionalProperties": False, "minItems": 0 } } ITEM_TEMPLATE = { "type": "integer", "minimum": 0, "exclusiveMinimum": 0.0, "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]