318 lines
11 KiB
Python
318 lines
11 KiB
Python
# Copyright 2014: 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 abc
|
|
import collections
|
|
|
|
import six
|
|
|
|
from rally.common import cfg
|
|
from rally.common import logging
|
|
from rally.common.plugin import plugin
|
|
from rally.common import utils
|
|
from rally.common import validation
|
|
from rally.task import atomic
|
|
from rally.task import functional
|
|
from rally.task import utils as task_utils
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
CONF_OPTS = [
|
|
cfg.StrOpt(
|
|
"context_resource_name_format",
|
|
help="Template is used to generate random names of resources. X is "
|
|
"replaced with random latter, amount of X can be adjusted")
|
|
]
|
|
CONF.register_opts(CONF_OPTS)
|
|
|
|
|
|
@logging.log_deprecated_args("Use 'platform' arg instead", "0.10.0",
|
|
["namespace"], log_function=LOG.warning)
|
|
def configure(name, order, platform="default", namespace=None, hidden=False):
|
|
"""Context class wrapper.
|
|
|
|
Each context class has to be wrapped by configure() wrapper. It
|
|
sets essential configuration of context classes. Actually this wrapper just
|
|
adds attributes to the class.
|
|
|
|
:param name: Name of the class, used in the input task
|
|
:param platform: str plugin's platform
|
|
:param order: As far as we can use multiple context classes that sometimes
|
|
depend on each other we have to specify order of execution.
|
|
Contexts with smaller order are run first
|
|
:param hidden: If it is true you won't be able to specify context via
|
|
task config
|
|
"""
|
|
if namespace:
|
|
platform = namespace
|
|
|
|
def wrapper(cls):
|
|
cls = plugin.configure(name=name, platform=platform,
|
|
hidden=hidden)(cls)
|
|
cls._meta_set("order", order)
|
|
return cls
|
|
|
|
return wrapper
|
|
|
|
|
|
def add_default_context(name, config):
|
|
"""Add default context that is inherit by all children plugins.
|
|
|
|
:param name: str, name of the validator plugin
|
|
:param kwargs: dict, arguments used to initialize validator class
|
|
instance
|
|
"""
|
|
|
|
def wrapper(plugin):
|
|
plugin._default_meta_setdefault("default_context", {})
|
|
plugin._default_meta_get("default_context")[name] = config
|
|
return plugin
|
|
|
|
return wrapper
|
|
|
|
|
|
# TODO(andreykurilin): BaseContext is used by Task and Verification and should
|
|
# be moved to common place
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class BaseContext(plugin.Plugin, functional.FunctionalMixin,
|
|
utils.RandomNameGeneratorMixin, atomic.ActionTimerMixin):
|
|
"""This class is a factory for context classes.
|
|
|
|
Every context class should be a subclass of this class and implement
|
|
2 abstract methods: setup() and cleanup()
|
|
|
|
It covers:
|
|
1) proper setting up of context config
|
|
2) Auto discovering & get by name
|
|
3) Validation by CONFIG_SCHEMA
|
|
4) Order of context creation
|
|
|
|
"""
|
|
RESOURCE_NAME_FORMAT = "c_rally_XXXXXXXX_XXXXXXXX"
|
|
|
|
CONFIG_SCHEMA = {"type": "null"}
|
|
|
|
def __init__(self, ctx):
|
|
super(BaseContext, self).__init__()
|
|
config = ctx.get("config", {})
|
|
if self.get_name() in config:
|
|
# TODO(boris-42): Fix tests, code is always using fullnames
|
|
config = config[self.get_name()]
|
|
else:
|
|
# TODO(boris-42): use [] instead of get() context full name is
|
|
# always presented.
|
|
config = config.get(self.get_fullname(), {})
|
|
# NOTE(amaretskiy): self.config is a constant data and must be
|
|
# immutable or write-protected type to prevent
|
|
# unexpected changes in runtime
|
|
if isinstance(config, dict):
|
|
if hasattr(self, "DEFAULT_CONFIG"):
|
|
for key, value in self.DEFAULT_CONFIG.items():
|
|
config.setdefault(key, value)
|
|
self.config = utils.LockedDict(config)
|
|
elif isinstance(config, list):
|
|
self.config = tuple(config)
|
|
else:
|
|
# NOTE(amaretskiy): It is improbable that config can be a None,
|
|
# number, boolean or even string,
|
|
# however we handle this
|
|
self.config = config
|
|
self.context = ctx
|
|
self.env = self.context.get("env", {})
|
|
|
|
@classmethod
|
|
def get_order(cls):
|
|
return cls._meta_get("order")
|
|
|
|
@abc.abstractmethod
|
|
def setup(self):
|
|
"""Prepare environment for test.
|
|
|
|
This method is executed only once before load generation.
|
|
|
|
self.config contains input arguments of this context
|
|
self.context contains information that will be passed to scenario
|
|
|
|
The goal of this method is to perform all operation to prepare
|
|
environment and store information to self.context that is required
|
|
by scenario.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def cleanup(self):
|
|
"""Clean up environment after load generation.
|
|
|
|
This method is run once after load generation is done to cleanup
|
|
environment.
|
|
|
|
self.config contains input arguments of this context
|
|
self.context contains information that was passed to scenario
|
|
"""
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
self.cleanup()
|
|
|
|
def __eq__(self, other):
|
|
return self.get_order() == other.get_order()
|
|
|
|
def __lt__(self, other):
|
|
return self.get_order() < other.get_order()
|
|
|
|
def __gt__(self, other):
|
|
return self.get_order() > other.get_order()
|
|
|
|
def __le__(self, other):
|
|
return self.get_order() <= other.get_order()
|
|
|
|
def __ge__(self, other):
|
|
return self.get_order() >= other.get_order()
|
|
|
|
|
|
@validation.add_default("jsonschema")
|
|
@plugin.base()
|
|
class Context(BaseContext, validation.ValidatablePluginMixin):
|
|
"""The base class for task contexts."""
|
|
def __init__(self, ctx):
|
|
super(Context, self).__init__(ctx)
|
|
self.task = self.context.get("task", {})
|
|
|
|
@classmethod
|
|
def _get_resource_name_format(cls):
|
|
return (CONF.context_resource_name_format
|
|
or super(Context, cls)._get_resource_name_format())
|
|
|
|
def get_owner_id(self):
|
|
if "owner_id" in self.context:
|
|
return self.context["owner_id"]
|
|
return super(Context, self).get_owner_id()
|
|
|
|
|
|
class ContextManager(object):
|
|
"""Create context environment and run method inside it."""
|
|
|
|
def __init__(self, context_obj):
|
|
self._visited = []
|
|
self.context_obj = context_obj
|
|
self._data = collections.OrderedDict()
|
|
|
|
def contexts_results(self):
|
|
"""Returns a list with contexts execution results."""
|
|
return list(self._data.values())
|
|
|
|
def _get_sorted_context_lst(self):
|
|
ctx_lst = [Context.get(name, allow_hidden=True)
|
|
for name in self.context_obj["config"]]
|
|
ctx_lst.sort(key=lambda x: x.get_order())
|
|
return [c(self.context_obj) for c in ctx_lst]
|
|
|
|
def _log_prefix(self):
|
|
return "Task %s |" % self.context_obj["task"]["uuid"]
|
|
|
|
def setup(self):
|
|
"""Creates environment by executing provided context plugins."""
|
|
self._visited = []
|
|
for ctx in self._get_sorted_context_lst():
|
|
ctx_data = {
|
|
"plugin_name": ctx.get_fullname(),
|
|
"plugin_cfg": ctx.config,
|
|
"setup": {
|
|
"started_at": None,
|
|
"finished_at": None,
|
|
"atomic_actions": None,
|
|
"error": None
|
|
},
|
|
"cleanup": {
|
|
"started_at": None,
|
|
"finished_at": None,
|
|
"atomic_actions": None,
|
|
"error": None
|
|
}
|
|
}
|
|
self._data[ctx.get_fullname()] = ctx_data
|
|
self._visited.append(ctx)
|
|
msg = ("%(log_prefix)s Context %(name)s setup() "
|
|
% {"log_prefix": self._log_prefix(),
|
|
"name": ctx.get_fullname()})
|
|
|
|
timer = utils.Timer()
|
|
try:
|
|
with timer:
|
|
ctx.setup()
|
|
except Exception as exc:
|
|
ctx_data["setup"]["error"] = task_utils.format_exc(exc)
|
|
raise
|
|
finally:
|
|
ctx_data["setup"]["atomic_actions"] = ctx.atomic_actions()
|
|
ctx_data["setup"]["started_at"] = timer.timestamp()
|
|
ctx_data["setup"]["finished_at"] = timer.finish_timestamp()
|
|
|
|
LOG.info("%(msg)s finished in %(duration)s"
|
|
% {"msg": msg, "duration": timer.duration(fmt=True)})
|
|
|
|
return self.context_obj
|
|
|
|
def cleanup(self):
|
|
"""Cleans up environment by executing provided context plugins."""
|
|
ctxlst = self._visited or self._get_sorted_context_lst()
|
|
for ctx in ctxlst[::-1]:
|
|
ctx.reset_atomic_actions()
|
|
msg = ("%(log_prefix)s Context %(name)s cleanup()"
|
|
% {"log_prefix": self._log_prefix(),
|
|
"name": ctx.get_fullname()})
|
|
# NOTE(andreykurilin): As for our code, ctx_data is
|
|
# always presented. The further checks for `ctx_data is None` are
|
|
# added just for "disaster cleanup". It is not officially
|
|
# presented feature and not we provide out-of-the-box, but some
|
|
# folks have own scripts which are based on ContextManager and
|
|
# it would be nice to not break them.
|
|
ctx_data = None
|
|
if ctx.get_fullname() in self._data:
|
|
ctx_data = self._data[ctx.get_fullname()]
|
|
|
|
timer = utils.Timer()
|
|
try:
|
|
with timer:
|
|
LOG.info("%s started" % msg)
|
|
ctx.cleanup()
|
|
LOG.info("%(msg)s finished in %(duration)s"
|
|
% {"msg": msg, "duration": timer.duration(fmt=True)})
|
|
except Exception as exc:
|
|
LOG.exception(
|
|
"%(msg)s failed after %(duration)s"
|
|
% {"msg": msg, "duration": timer.duration(fmt=True)})
|
|
if ctx_data is not None:
|
|
ctx_data["cleanup"]["error"] = task_utils.format_exc(exc)
|
|
finally:
|
|
if ctx_data is not None:
|
|
aa = ctx.atomic_actions()
|
|
ctx_data["cleanup"]["atomic_actions"] = aa
|
|
ctx_data["cleanup"]["started_at"] = timer.timestamp()
|
|
finished_at = timer.finish_timestamp()
|
|
ctx_data["cleanup"]["finished_at"] = finished_at
|
|
|
|
def __enter__(self):
|
|
try:
|
|
self.setup()
|
|
except Exception:
|
|
self.cleanup()
|
|
raise
|
|
|
|
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
self.cleanup()
|