1. 如何使用
context的使用,可以在编写脚本时配置,也可以在yaml文件里配置,具体要看测试的场景,最终脚本的配置与yaml文件的配置合并生效(yaml文件里的配置优先)
1.1 脚本
代码: rally-openstack/rally_openstack/scenarios/nova/servers.py
@types.convert(image={"type": "glance_image"},
flavor={"type": "nova_flavor"})
@validation.add("image_valid_on_flavor", flavor_param="flavor",
image_param="image")
@validation.add("external_network_exists", param_name="floating_network")
@validation.add("required_services", services=[consts.Service.NOVA])
@validation.add("required_platform", platform="openstack", users=True)
@scenario.configure(context={"cleanup@openstack": ["nova"],
"allow_ssh@openstack": None},
name="NovaServers.boot_and_change_password",
platform="openstack")
class BootAndChangePassword(vm_utils.VMScenario):
def run(self, image, flavor, password, username="root",
min_sleep=15, max_sleep=20, floating_network=None,
port=22, use_floating_ip=True, force_delete=False,
...
编写测试例脚本时,需要使用第8行@scenario.configure(context={"context_name@platform": args, ...}, ...)来声明
例子中 allow_ssh就是在测试执行前创建security group,放行所有的规则,在测试结束后删除security group。
代码:rally-openstack/rally_openstack/scenarios/nova/utils.py
@atomic.action_timer("nova.boot_server")
def _boot_server(self, image, flavor,
auto_assign_nic=False, **kwargs):
"""Boot a server.
Returns when the server is actually booted and in "ACTIVE" state.
If multiple networks created by Network context are present, the first
network found that isn't associated with a floating IP pool is used.
:param image: image ID or instance for server creation
:param flavor: int, flavor ID or instance for server creation
:param auto_assign_nic: bool, whether or not to auto assign NICs
:param kwargs: other optional parameters to initialize the server
:returns: nova Server instance
"""
server_name = self.generate_random_name()
secgroup = self.context.get("user", {}).get("secgroup")
if secgroup:
if "security_groups" not in kwargs:
kwargs["security_groups"] = [secgroup["name"]]
elif secgroup["name"] not in kwargs["security_groups"]:
kwargs["security_groups"].append(secgroup["name"])
context创建的资源存放在self.context中,第18行说明了在脚本中如何获取context创建的secgroup,具体self.context的结构,我们在后面说明
ps: cleanup作为一种特殊的context,我们另起文章说明
1.2 yaml文件
yaml文件:rally-openstack/CT-jobs/nova.yaml
-
description: >
This is also used in Testlink for server change password
title: NovaServers.boot_and_change_password tests
scenario:
NovaServers.boot_and_change_password:
flavor:
name: {{flavor_name}}
image:
name: {{image_name}}
floating_network: {{ext_network}}
password: "change"
runner:
constant:
times: 1
concurrency: 1
contexts:
network: {}
users:
tenants: 2
users_per_tenant: 1
yaml文件中context的使用,请见17-21行,在测试例执行前需要创建资源 tenant,user,network
1.3 资源的配置参数
我们以network为例,上述yaml文件中使用的是{},即默认参数
代码:rally-openstack/rally_openstack/contexts/network/networks.py
@validation.add("required_platform", platform="openstack", admin=True,
users=True)
@context.configure(name="network", platform="openstack", order=350)
class Network(context.Context):
"""Create networking resources.
This creates networks for all tenants, and optionally creates
another resources like subnets and routers.
"""
CONFIG_SCHEMA = {
"type": "object",
"$schema": consts.JSON_SCHEMA,
"properties": {
"start_cidr": {
"type": "string"
},
"networks_per_tenant": {
"type": "integer",
"minimum": 1
},
"subnets_per_network": {
"type": "integer",
"minimum": 1
},
"network_create_args": {
"type": "object",
"additionalProperties": True
},
"dns_nameservers": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": True
},
"dualstack": {
"type": "boolean",
},
"router": {
"type": "object",
"properties": {
"external": {
"type": "boolean"
},
"external_gateway_info": {
"description": "The external gateway information .",
"type": "object",
"properties": {
"network_id": {"type": "string"},
"enable_snat": {"type": "boolean"}
},
"additionalProperties": False
}
},
"additionalProperties": False
}
},
"additionalProperties": False
}
DEFAULT_CONFIG = {
"start_cidr": "10.2.0.0/24",
"networks_per_tenant": 1,
"subnets_per_network": 1,
"network_create_args": {},
"dns_nameservers": None,
"router": {"external": True},
"dualstack": False
}
第11-58行 CONFIG_SCHEMA定义了参数的配置规范,说明了network可以配置的参数及参数值的类型和范围
第60-68行 DEFAULT_CONFIG定义了默认参数,当network的某一个参数未定义时,会使用默认参数,见如下代码的34-36行
代码:rally/task/context.py
@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", {})
ps: 上述代码说明context的参数最好是dict或者list
2. 如何开发
我们继续以network为例
代码:rally-openstack/rally_openstack/contexts/network/networks.py
# NOTE(andreykurilin): admin is used only by cleanup
@validation.add("required_platform", platform="openstack", admin=True,
users=True)
@context.configure(name="network", platform="openstack", order=350)
class Network(context.Context):
"""Create networking resources.
This creates networks for all tenants, and optionally creates
another resources like subnets and routers.
"""
...
def setup(self):
# NOTE(rkiran): Some clients are not thread-safe. Thus during
# multithreading/multiprocessing, it is likely the
# sockets are left open. This problem is eliminated by
# creating a connection in setup and cleanup separately.
net_wrapper = network_wrapper.wrap(
osclients.Clients(self.context["admin"]["credential"]),
self, config=self.config)
kwargs = {}
if self.config["dns_nameservers"] is not None:
kwargs["dns_nameservers"] = self.config["dns_nameservers"]
for user, tenant_id in (utils.iterate_per_tenants(
self.context.get("users", []))):
self.context["tenants"][tenant_id]["networks"] = []
for i in range(self.config["networks_per_tenant"]):
# NOTE(amaretskiy): router_create_args and subnets_num take
# effect for Neutron only.
network_create_args = self.config["network_create_args"].copy()
network = net_wrapper.create_network(
tenant_id,
dualstack=self.config["dualstack"],
subnets_num=self.config["subnets_per_network"],
network_create_args=network_create_args,
router_create_args=self.config["router"],
**kwargs)
self.context["tenants"][tenant_id]["networks"].append(network)
def cleanup(self):
net_wrapper = network_wrapper.wrap(
osclients.Clients(self.context["admin"]["credential"]),
self, config=self.config)
for tenant_id, tenant_ctx in self.context["tenants"].items():
for network in tenant_ctx.get("networks", []):
with logging.ExceptionLogger(
LOG,
"Failed to delete network for tenant %s" % tenant_id):
net_wrapper.delete_network(network)
第4行:@context.configure(name="network", platform="openstack", order=350)
name表示上下文的名称;platform表示平台的名称;order表示在上下文中执行的顺序,setup中越小越先执行,cleanup中越大越先执行,在编写中要根据实际情况,赋予合适的顺序号
另外context.configure还可以有一个参数 hidden:设置为True时,不能在yaml文件的上下文中设置该项目
代码:rally/task/context.py
@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
2.1 class Contex
network继承Context,实现了setup和cleanup两个方法
代码:rally/task/context.py
@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()
@validation.add_default("jsonschema")
@plugin.base()
class Context(BaseContext, validation.ValidatablePluginMixin):
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()
第16行:RESOURCE_NAME_FORMAT = "c_rally_XXXXXXXX_XXXXXXXX"表示在上下文中创建的资源,默认的名称格式
第91-93行: 上下文中创建的资源名称也可以通过CONF.context_resource_name_format来设置
第37行: 上下文项目的参数保存在实例的self.config中
第53,67行: setup和cleanup是抽象方法,需要在子类实现
2.2 ContextManager
ContextManager 控制着上下文的运行
代码:rally/task/context.py
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()
第13-17行:_get_sorted_context_lst方法会将脚本和yaml文件设置的上下文项目按照order排序,返回的结果[c(self.context_obj) for c in ctx_lst]是各上下文项目的实例
第22-102行:setup和cleanup方法依次执行排序后的上下文项目的setup和cleanup
第104-112行:__enter__和__exit__方法定义,支持使用with
3. rally如何运行Context
查找命令rally task start的运行
代码:task/engine.py
def _run_workload(self, subtask_obj, workload):
if ResultConsumer.is_task_in_aborting_status(self.task["uuid"]):
raise TaskAborted()
workload_obj = subtask_obj.add_workload(
name=workload["name"],
description=workload["description"],
position=workload["position"],
runner=workload["runner"],
runner_type=workload["runner_type"],
hooks=workload["hooks"],
contexts=workload["contexts"],
sla=workload["sla"],
args=workload["args"])
workload["uuid"] = workload_obj["uuid"]
workload_cfg = objects.Workload.to_task(workload)
LOG.info("Running workload:
"
" position = %(position)s
"
" config = %(cfg)s"
% {"position": workload["position"],
"cfg": json.dumps(workload_cfg, indent=3)})
runner_cls = runner.ScenarioRunner.get(workload["runner_type"])
runner_obj = runner_cls(self.task, workload["runner"])
context_obj = self._prepare_context(
workload["contexts"], workload["name"], workload_obj["uuid"])
try:
ctx_manager = context.ContextManager(context_obj)
with ResultConsumer(workload, task=self.task, subtask=subtask_obj,
workload=workload_obj, runner=runner_obj,
abort_on_sla_failure=self.abort_on_sla_failure,
ctx_manager=ctx_manager):
with ctx_manager:
runner_obj.run(workload["name"], context_obj,
workload["args"])
except Exception:
LOG.exception("Unexpected exception during the workload execution")
# TODO(astudenov): save error to DB
第25,26行:根据配置文件,生成上下文对象
第33行: 使用with语句,执行上下文中各个对象的setup和cleanup
代码:task/engine.py
def _prepare_context(self, ctx, scenario_name, owner_id):
context_config = {}
# restore full names of plugins
scenario_plugin = scenario.Scenario.get(scenario_name)
for k, v in scenario_plugin.get_default_context().items():
c = context.Context.get(k, allow_hidden=True)
context_config[c.get_fullname()] = v
for k, v in ctx.items():
context_config[context.Context.get(k).get_fullname()] = v
env_data = self.env.data
env_data["platforms"] = dict(
(p["platform_name"], p["platform_data"])
for p in env_data["platforms"].values())
context_obj = {
"task": self.task,
"owner_id": owner_id,
"scenario_name": scenario_name,
"config": context_config,
"env": env_data
}
return context_obj
第4-7行: 加载脚本中对context的设置,
第8-9行: 加载yaml文件中对context的设置,其中context.Context.get(k)会过滤掉hidden=True的设置,见下面第21,22行
代码:rally/common/plugin/plugin.py
@classmethod
def get_all(cls, platform=None, allow_hidden=False, name=None):
"""Return all subclass plugins of plugin.
All plugins that are not configured will be ignored.
:param platform: return only plugins for specific platform.
:param name: return only plugins with specified name.
:param allow_hidden: if False return only non hidden plugins
"""
plugins = []
for p in discover.itersubclasses(cls):
if not issubclass(p, Plugin):
continue
if not p._meta_is_inited(raise_exc=False):
continue
if name and name != p.get_name():
continue
if platform and platform != p.get_platform():
continue
if not allow_hidden and p.is_hidden():
continue
plugins.append(p)
return plugins
_prepare_context返回的上下文对象的结构为
{
"task": self.task,
"owner_id": owner_id,
"scenario_name": scenario_name,
"config": context_config,
"env": env_data
}
这就是上下文self.context的初始内容,根据各上下文项目的执行,各个资源的信息将被存储在上下文实例的self.context中,在测试并发运行阶段rally会将self.context深拷贝到运行实例中
ps: self.context["env"] 保存着rally deployment或者rally env的信息