1144 lines
45 KiB
Python
1144 lines
45 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 os
|
|
import re
|
|
import sys
|
|
import time
|
|
|
|
import jinja2
|
|
import jinja2.meta
|
|
import jsonschema
|
|
from oslo_config import cfg
|
|
from requests.packages import urllib3
|
|
|
|
from rally.common.i18n import _, _LI, _LE, _LW
|
|
from rally.common import logging
|
|
from rally.common import objects
|
|
from rally.common.plugin import discover
|
|
from rally.common import utils
|
|
from rally.common import version as rally_version
|
|
from rally import consts
|
|
from rally.deployment import engine as deploy_engine
|
|
from rally import exceptions
|
|
from rally import osclients
|
|
from rally.task import engine
|
|
from rally.verification import context as vcontext
|
|
from rally.verification import manager as vmanager
|
|
from rally.verification import reporter as vreporter
|
|
|
|
|
|
CONF = cfg.CONF
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class _Deployment(object):
|
|
|
|
@classmethod
|
|
def create(cls, config, name):
|
|
"""Create a deployment.
|
|
|
|
:param config: a dict with deployment configuration
|
|
:param name: a str represents a name of the deployment
|
|
:returns: Deployment object
|
|
"""
|
|
|
|
try:
|
|
deployment = objects.Deployment(name=name, config=config)
|
|
except exceptions.DeploymentNameExists as e:
|
|
if logging.is_debug():
|
|
LOG.exception(e)
|
|
raise
|
|
|
|
deployer = deploy_engine.Engine.get_engine(
|
|
deployment["config"]["type"], deployment)
|
|
try:
|
|
deployer.validate()
|
|
except jsonschema.ValidationError:
|
|
LOG.error(_LE("Deployment %s: Schema validation error.") %
|
|
deployment["uuid"])
|
|
deployment.update_status(consts.DeployStatus.DEPLOY_FAILED)
|
|
raise
|
|
|
|
with deployer:
|
|
credentials = deployer.make_deploy()
|
|
deployment.update_credentials(credentials)
|
|
return deployment
|
|
|
|
@classmethod
|
|
def destroy(cls, deployment):
|
|
"""Destroy the deployment.
|
|
|
|
:param deployment: UUID or name of the deployment
|
|
"""
|
|
# TODO(akscram): We have to be sure that there are no running
|
|
# tasks for this deployment.
|
|
# TODO(akscram): Check that the deployment have got a status that
|
|
# is equal to "*->finished" or "deploy->inconsistent".
|
|
deployment = objects.Deployment.get(deployment)
|
|
try:
|
|
deployer = deploy_engine.Engine.get_engine(
|
|
deployment["config"]["type"], deployment)
|
|
with deployer:
|
|
deployer.make_cleanup()
|
|
except exceptions.PluginNotFound:
|
|
LOG.info(_("Deployment %s will be deleted despite exception")
|
|
% deployment["uuid"])
|
|
|
|
for verifier in _Verifier.list():
|
|
_Verifier.delete(verifier.name, deployment["name"], force=True)
|
|
|
|
deployment.delete()
|
|
|
|
@classmethod
|
|
def recreate(cls, deployment, config=None):
|
|
"""Performs a cleanup and then makes a deployment again.
|
|
|
|
:param deployment: UUID or name of the deployment
|
|
:param config: an optional dict with deployment config to update before
|
|
redeploy
|
|
"""
|
|
deployment = objects.Deployment.get(deployment)
|
|
deployer = deploy_engine.Engine.get_engine(
|
|
deployment["config"]["type"], deployment)
|
|
|
|
if config:
|
|
if deployment["config"]["type"] != config["type"]:
|
|
raise exceptions.RallyException(
|
|
"Can't change deployment type.")
|
|
try:
|
|
deployer.validate(config)
|
|
except jsonschema.ValidationError:
|
|
LOG.error(_LE("Config schema validation error."))
|
|
raise
|
|
|
|
with deployer:
|
|
deployer.make_cleanup()
|
|
|
|
if config:
|
|
deployment.update_config(config)
|
|
|
|
credentials = deployer.make_deploy()
|
|
deployment.update_credentials(credentials)
|
|
|
|
@classmethod
|
|
def get(cls, deployment):
|
|
"""Get the deployment.
|
|
|
|
:param deployment: UUID or name of the deployment
|
|
:returns: Deployment instance
|
|
"""
|
|
return objects.Deployment.get(deployment)
|
|
|
|
@classmethod
|
|
def service_list(cls, deployment):
|
|
"""Get the services list.
|
|
|
|
:param deployment: Deployment object
|
|
:returns: Service list
|
|
"""
|
|
# TODO(astudenov): put this work into Credential plugins
|
|
admin = deployment.get_credentials_for("openstack")["admin"]
|
|
clients = osclients.Clients(objects.Credential(**admin))
|
|
return clients.services()
|
|
|
|
@staticmethod
|
|
def list(status=None, parent_uuid=None, name=None):
|
|
"""Get the deployments list.
|
|
|
|
:returns: Deployment list
|
|
"""
|
|
return objects.Deployment.list(status, parent_uuid, name)
|
|
|
|
@classmethod
|
|
def check(cls, deployment):
|
|
"""Check keystone authentication and list all available services.
|
|
|
|
:returns: Service list
|
|
"""
|
|
# TODO(astudenov): put this work into Credential plugins
|
|
services = cls.service_list(deployment)
|
|
users = deployment.get_credentials_for("openstack")["users"]
|
|
for endpoint_dict in users:
|
|
osclients.Clients(objects.Credential(**endpoint_dict)).keystone()
|
|
|
|
return services
|
|
|
|
|
|
class _Task(object):
|
|
|
|
TASK_RESULT_SCHEMA = objects.task.TASK_RESULT_SCHEMA
|
|
|
|
@staticmethod
|
|
def list(**filters):
|
|
return objects.Task.list(**filters)
|
|
|
|
@staticmethod
|
|
def get(task_id):
|
|
return objects.Task.get(task_id)
|
|
|
|
@staticmethod
|
|
def get_detailed(task_id, extended_results=False):
|
|
"""Get detailed task data.
|
|
|
|
:param task_id: str task UUID
|
|
:param extended_results: whether to return task data as dict
|
|
with extended results
|
|
:returns: rally.common.db.sqlalchemy.models.Task
|
|
:returns: dict
|
|
"""
|
|
task = objects.Task.get_detailed(task_id)
|
|
if task and extended_results:
|
|
task = dict(task)
|
|
task["results"] = objects.Task.extend_results(task["results"])
|
|
return task
|
|
|
|
@classmethod
|
|
def render_template(cls, task_template, template_dir="./", **kwargs):
|
|
"""Render jinja2 task template to Rally input task.
|
|
|
|
:param task_template: String that contains template
|
|
:param template_dir: The path of directory contain template files
|
|
:param kwargs: Dict with template arguments
|
|
:returns: rendered template str
|
|
"""
|
|
|
|
def is_really_missing(mis, task_template):
|
|
# NOTE(boris-42): Removing variables that have default values from
|
|
# missing. Construction that won't be properly
|
|
# checked is {% set x = x or 1}
|
|
if re.search(mis.join(["{%\s*set\s+", "\s*=\s*", "[^\w]+"]),
|
|
task_template):
|
|
return False
|
|
# NOTE(jlk): Also check for a default filter which can show up as
|
|
# a missing variable
|
|
if re.search(mis + "\s*\|\s*default\(", task_template):
|
|
return False
|
|
return True
|
|
|
|
# NOTE(boris-42): We have to import builtins to get the full list of
|
|
# builtin functions (e.g. range()). Unfortunately,
|
|
# __builtins__ doesn't return them (when it is not
|
|
# main module)
|
|
from six.moves import builtins
|
|
|
|
env = jinja2.Environment(
|
|
loader=jinja2.FileSystemLoader(template_dir, encoding="utf8"))
|
|
env.globals.update(cls.create_template_functions())
|
|
ast = env.parse(task_template)
|
|
# NOTE(Julia Varigina):
|
|
# Bug in jinja2.meta.find_undeclared_variables
|
|
#
|
|
# The method shows inconsistent behavior:
|
|
# it does not return undeclared variables that appear
|
|
# in included templates only (via {%- include "some_template.yaml"-%})
|
|
# and in the same time is declared in jinja2.Environment.globals.
|
|
#
|
|
# This is different for undeclared variables that appear directly
|
|
# in task_template. The method jinja2.meta.find_undeclared_variables
|
|
# returns an undeclared variable that is used in task_template
|
|
# and is set in jinja2.Environment.globals.
|
|
#
|
|
# Despite this bug, jinja resolves values
|
|
# declared in jinja2.Environment.globals for both types of undeclared
|
|
# variables and successfully renders templates in both cases.
|
|
required_kwargs = jinja2.meta.find_undeclared_variables(ast)
|
|
missing = (set(required_kwargs) - set(kwargs) - set(dir(builtins)) -
|
|
set(env.globals))
|
|
real_missing = [mis for mis in missing
|
|
if is_really_missing(mis, task_template)]
|
|
if real_missing:
|
|
multi_msg = _("Please specify next template task arguments: %s")
|
|
single_msg = _("Please specify template task argument: %s")
|
|
|
|
raise TypeError((len(real_missing) > 1 and multi_msg or single_msg)
|
|
% ", ".join(real_missing))
|
|
|
|
return env.from_string(task_template).render(**kwargs)
|
|
|
|
@classmethod
|
|
def create_template_functions(cls):
|
|
|
|
def template_min(int1, int2):
|
|
return min(int1, int2)
|
|
|
|
def template_max(int1, int2):
|
|
return max(int1, int2)
|
|
|
|
def template_round(float1):
|
|
return int(round(float1))
|
|
|
|
def template_ceil(float1):
|
|
import math
|
|
return int(math.ceil(float1))
|
|
|
|
return {"min": template_min, "max": template_max,
|
|
"ceil": template_ceil, "round": template_round}
|
|
|
|
@classmethod
|
|
def create(cls, deployment, tag):
|
|
"""Create a task without starting it.
|
|
|
|
Task is a list of benchmarks that will be called one by one, results of
|
|
execution will be stored in DB.
|
|
|
|
:param deployment: UUID or name of the deployment
|
|
:param tag: tag for this task
|
|
:returns: Task object
|
|
"""
|
|
|
|
deployment = objects.Deployment.get(deployment)
|
|
if deployment["status"] != consts.DeployStatus.DEPLOY_FINISHED:
|
|
raise exceptions.DeploymentNotFinishedStatus(
|
|
name=deployment["name"],
|
|
uuid=deployment["uuid"],
|
|
status=deployment["status"])
|
|
|
|
return objects.Task(deployment_uuid=deployment["uuid"],
|
|
tag=tag)
|
|
|
|
@classmethod
|
|
def validate(cls, deployment, config, task_instance=None):
|
|
"""Validate a task config against specified deployment.
|
|
|
|
:param deployment: UUID or name of the deployment
|
|
:param config: a dict with a task configuration
|
|
"""
|
|
deployment = objects.Deployment.get(deployment)
|
|
task = task_instance or objects.Task(
|
|
deployment_uuid=deployment["uuid"], temporary=True)
|
|
benchmark_engine = engine.TaskEngine(config, task, deployment)
|
|
|
|
benchmark_engine.validate()
|
|
|
|
@classmethod
|
|
def start(cls, deployment, config, task=None, abort_on_sla_failure=False):
|
|
"""Start a task.
|
|
|
|
Task is a list of benchmarks that will be called one by one, results of
|
|
execution will be stored in DB.
|
|
|
|
:param deployment: UUID or name of the deployment
|
|
:param config: a dict with a task configuration
|
|
:param task: Task object. If None, it will be created
|
|
:param abort_on_sla_failure: If set to True, the task execution will
|
|
stop when any SLA check for it fails
|
|
"""
|
|
deployment = objects.Deployment.get(deployment)
|
|
|
|
task = task or objects.Task(deployment_uuid=deployment["uuid"])
|
|
|
|
if task.is_temporary:
|
|
raise ValueError(_(
|
|
"Unable to run a temporary task. Please check your code."))
|
|
|
|
LOG.info("Benchmark Task %s on Deployment %s" % (task["uuid"],
|
|
deployment["uuid"]))
|
|
|
|
benchmark_engine = engine.TaskEngine(
|
|
config, task, deployment,
|
|
abort_on_sla_failure=abort_on_sla_failure)
|
|
|
|
try:
|
|
benchmark_engine.run()
|
|
except Exception:
|
|
deployment.update_status(consts.DeployStatus.DEPLOY_INCONSISTENT)
|
|
raise
|
|
|
|
@classmethod
|
|
def abort(cls, task_uuid, soft=False, async=True):
|
|
"""Abort running task.
|
|
|
|
:param task_uuid: The UUID of the task
|
|
:type task_uuid: str
|
|
:param soft: If set to True, task should be aborted after execution of
|
|
current scenario, otherwise as soon as possible before
|
|
all the scenario iterations finish [Default: False]
|
|
:type soft: bool
|
|
:param async: don't wait until task became in 'running' state
|
|
[Default: False]
|
|
:type async: bool
|
|
"""
|
|
|
|
if not async:
|
|
current_status = objects.Task.get_status(task_uuid)
|
|
if current_status in objects.Task.NOT_IMPLEMENTED_STAGES_FOR_ABORT:
|
|
LOG.info(_LI("Task status is '%s'. Should wait until it became"
|
|
" 'running'") % current_status)
|
|
while (current_status in
|
|
objects.Task.NOT_IMPLEMENTED_STAGES_FOR_ABORT):
|
|
time.sleep(1)
|
|
current_status = objects.Task.get_status(task_uuid)
|
|
|
|
objects.Task.get(task_uuid).abort(soft=soft)
|
|
|
|
if not async:
|
|
LOG.info(_LI("Waiting until the task stops."))
|
|
finished_stages = [consts.TaskStatus.ABORTED,
|
|
consts.TaskStatus.FINISHED,
|
|
consts.TaskStatus.CRASHED]
|
|
while objects.Task.get_status(task_uuid) not in finished_stages:
|
|
time.sleep(1)
|
|
|
|
@classmethod
|
|
def delete(cls, task_uuid, force=False):
|
|
"""Delete the task.
|
|
|
|
:param task_uuid: The UUID of the task
|
|
:param force: If set to True, then delete the task despite to the
|
|
status
|
|
:raises TaskInvalidStatus: when the status of the task is not
|
|
in FINISHED, FAILED or ABORTED and
|
|
the force argument is not True
|
|
"""
|
|
if force:
|
|
objects.Task.delete_by_uuid(task_uuid, status=None)
|
|
elif objects.Task.get_status(task_uuid) in (
|
|
consts.TaskStatus.ABORTED,
|
|
consts.TaskStatus.FINISHED,
|
|
consts.TaskStatus.CRASHED):
|
|
objects.Task.delete_by_uuid(task_uuid, status=None)
|
|
else:
|
|
objects.Task.delete_by_uuid(
|
|
task_uuid, status=consts.TaskStatus.FINISHED)
|
|
|
|
|
|
class _Verifier(object):
|
|
|
|
@classmethod
|
|
def list_plugins(cls, namespace=None):
|
|
"""List all plugins for verifiers management.
|
|
|
|
:param namespace: Verifier plugin namespace
|
|
"""
|
|
return [{"name": p.get_name(),
|
|
"namespace": p.get_namespace(),
|
|
"description": p.get_info()["title"],
|
|
"location": "%s.%s" % (p.__module__, p.__name__)}
|
|
for p in vmanager.VerifierManager.get_all(namespace=namespace)]
|
|
|
|
@classmethod
|
|
def create(cls, name, vtype, namespace=None, source=None, version=None,
|
|
system_wide=False, extra_settings=None):
|
|
"""Create a verifier.
|
|
|
|
:param name: Verifier name
|
|
:param vtype: Verifier plugin name
|
|
:param namespace: Verifier plugin namespace. Should be specified when
|
|
there are two verifier plugins with equal names but
|
|
in different namespaces
|
|
:param source: Path or URL to the repo to clone verifier from
|
|
:param version: Branch, tag or commit ID to checkout before
|
|
verifier installation
|
|
:param system_wide: Whether or not to use the system-wide environment
|
|
for verifier instead of a virtual environment
|
|
:param extra_settings: Extra installation settings for verifier
|
|
"""
|
|
# check that the specified verifier type exists
|
|
vmanager.VerifierManager.get(vtype, namespace=namespace)
|
|
|
|
LOG.info("Creating verifier '%s'.", name)
|
|
|
|
try:
|
|
verifier = cls.get(name)
|
|
except exceptions.ResourceNotFound:
|
|
verifier = objects.Verifier.create(
|
|
name=name, source=source, system_wide=system_wide,
|
|
version=version, vtype=vtype, namespace=namespace,
|
|
extra_settings=extra_settings)
|
|
else:
|
|
raise exceptions.RallyException(
|
|
"Verifier with name '%s' already exists! Please, specify "
|
|
"another name for verifier and try again." % verifier.name)
|
|
|
|
properties = {}
|
|
|
|
default_namespace = verifier.manager._meta_get("namespace")
|
|
if not namespace and default_namespace:
|
|
properties["namespace"] = default_namespace
|
|
|
|
default_source = verifier.manager._meta_get("default_repo")
|
|
if not source and default_source:
|
|
properties["source"] = default_source
|
|
|
|
if properties:
|
|
verifier.update_properties(**properties)
|
|
|
|
verifier.update_status(consts.VerifierStatus.INSTALLING)
|
|
try:
|
|
verifier.manager.install()
|
|
except Exception:
|
|
verifier.update_status(consts.VerifierStatus.FAILED)
|
|
raise
|
|
verifier.update_status(consts.VerifierStatus.INSTALLED)
|
|
|
|
LOG.info("Verifier %s has been successfully created!", verifier)
|
|
|
|
return verifier.uuid
|
|
|
|
@staticmethod
|
|
def get(verifier_id):
|
|
"""Get a verifier.
|
|
|
|
:param verifier_id: Verifier name or UUID
|
|
"""
|
|
return objects.Verifier.get(verifier_id)
|
|
|
|
@staticmethod
|
|
def list(status=None):
|
|
"""List all verifiers.
|
|
|
|
:param status: Status to filter verifiers by
|
|
"""
|
|
return objects.Verifier.list(status)
|
|
|
|
@classmethod
|
|
def delete(cls, verifier_id, deployment_id=None, force=False):
|
|
"""Delete a verifier.
|
|
|
|
:param verifier_id: Verifier name or UUID
|
|
:param deployment_id: Deployment name or UUID. If specified,
|
|
only the deployment-specific data will be deleted
|
|
for verifier
|
|
:param force: Delete all stored verifier verifications.
|
|
If deployment_id specified, only verifications of this
|
|
deployment will be deleted
|
|
"""
|
|
verifier = cls.get(verifier_id)
|
|
verifications = _Verification.list(verifier_id, deployment_id)
|
|
if verifications:
|
|
d_msg = ((" for deployment '%s'" % deployment_id)
|
|
if deployment_id else "")
|
|
if force:
|
|
LOG.info("Deleting all verifications created by verifier "
|
|
"%s%s.", verifier, d_msg)
|
|
for verification in verifications:
|
|
_Verification.delete(verification.uuid)
|
|
else:
|
|
raise exceptions.RallyException(
|
|
"Failed to delete verifier {0} because there are stored "
|
|
"verifier verifications{1}! Please, make sure that they "
|
|
"are not important to you. Use 'force' flag if you would "
|
|
"like to delete verifications{1} as well."
|
|
.format(verifier, d_msg))
|
|
|
|
if deployment_id:
|
|
LOG.info("Deleting deployment-specific data for verifier %s.",
|
|
verifier)
|
|
verifier.set_deployment(deployment_id)
|
|
verifier.manager.uninstall()
|
|
LOG.info("Deployment-specific data has been successfully deleted!")
|
|
else:
|
|
LOG.info("Deleting verifier %s.", verifier)
|
|
verifier.manager.uninstall(full=True)
|
|
objects.Verifier.delete(verifier_id)
|
|
LOG.info("Verifier has been successfully deleted!")
|
|
|
|
@classmethod
|
|
def update(cls, verifier_id, system_wide=None, version=None,
|
|
update_venv=False):
|
|
"""Update a verifier.
|
|
|
|
:param verifier_id: Verifier name or UUID
|
|
:param system_wide: Switch to using the system-wide environment
|
|
:param version: Branch, tag or commit ID to checkout
|
|
:param update_venv: Update the virtual environment for verifier
|
|
"""
|
|
if system_wide is None and version is None and not update_venv:
|
|
# nothing to update
|
|
raise exceptions.RallyException(
|
|
"At least one of the following parameters should be "
|
|
"specified: 'system_wide', 'version', 'update_venv'.")
|
|
|
|
verifier = cls.get(verifier_id)
|
|
LOG.info("Updating verifier %s.", verifier)
|
|
|
|
if verifier.status != consts.VerifierStatus.INSTALLED:
|
|
raise exceptions.RallyException(
|
|
"Failed to update verifier %s because verifier is in '%s' "
|
|
"status, but should be in '%s'." % (
|
|
verifier, verifier.status, consts.VerifierStatus.INSTALLED)
|
|
)
|
|
|
|
system_wide_in_use = (system_wide or
|
|
(system_wide is None and verifier.system_wide))
|
|
if update_venv and system_wide_in_use:
|
|
raise exceptions.RallyException(
|
|
"It is impossible to update the virtual environment for "
|
|
"verifier %s when it uses the system-wide environment."
|
|
% verifier)
|
|
|
|
# store original status to set it again after updating or rollback
|
|
original_status = verifier.status
|
|
verifier.update_status(consts.VerifierStatus.UPDATING)
|
|
|
|
properties = {} # store new verifier properties to update old ones
|
|
|
|
sw_is_checked = False
|
|
|
|
if version:
|
|
properties["version"] = version
|
|
|
|
backup = utils.BackupHelper()
|
|
rollback_msg = ("Failed to update verifier %s. It has been "
|
|
"rollbacked to the previous state." % verifier)
|
|
backup.add_rollback_action(LOG.info, rollback_msg)
|
|
backup.add_rollback_action(verifier.update_status, original_status)
|
|
with backup(verifier.manager.repo_dir):
|
|
verifier.manager.checkout(version)
|
|
|
|
if system_wide_in_use:
|
|
verifier.manager.check_system_wide()
|
|
sw_is_checked = True
|
|
|
|
if system_wide is not None:
|
|
if system_wide == verifier.system_wide:
|
|
LOG.info(
|
|
"Verifier %s is already switched to system_wide=%s. "
|
|
"Nothing will be changed.", verifier, verifier.system_wide)
|
|
else:
|
|
properties["system_wide"] = system_wide
|
|
if not system_wide:
|
|
update_venv = True # we need to install a virtual env
|
|
else:
|
|
# NOTE(andreykurilin): should we remove previously created
|
|
# virtual environment?!
|
|
if not sw_is_checked:
|
|
verifier.manager.check_system_wide()
|
|
|
|
if update_venv:
|
|
backup = utils.BackupHelper()
|
|
rollback_msg = ("Failed to update the virtual environment for "
|
|
"verifier %s. It has been rollbacked to the "
|
|
"previous state." % verifier)
|
|
backup.add_rollback_action(LOG.info, rollback_msg)
|
|
backup.add_rollback_action(verifier.update_status, original_status)
|
|
with backup(verifier.manager.venv_dir):
|
|
verifier.manager.install_venv()
|
|
|
|
properties["status"] = original_status # change verifier status back
|
|
verifier.update_properties(**properties)
|
|
|
|
LOG.info("Verifier %s has been successfully updated!", verifier)
|
|
|
|
return verifier.uuid
|
|
|
|
@classmethod
|
|
def configure(cls, verifier, deployment_id, extra_options=None,
|
|
reconfigure=False):
|
|
"""Configure a verifier.
|
|
|
|
:param verifier: Verifier object or (name or UUID)
|
|
:param deployment_id: Deployment name or UUID
|
|
:param extra_options: Extend verifier configuration with extra options
|
|
:param reconfigure: Reconfigure verifier
|
|
"""
|
|
if not isinstance(verifier, objects.Verifier):
|
|
verifier = cls.get(verifier)
|
|
verifier.set_deployment(deployment_id)
|
|
LOG.info(
|
|
"Configuring verifier %s for deployment '%s' (UUID=%s).",
|
|
verifier, verifier.deployment["name"], verifier.deployment["uuid"])
|
|
|
|
if verifier.status != consts.VerifierStatus.INSTALLED:
|
|
raise exceptions.RallyException(
|
|
"Failed to configure verifier %s for deployment '%s' "
|
|
"(UUID=%s) because verifier is in '%s' status, but should be "
|
|
"in '%s'." % (verifier, verifier.deployment["name"],
|
|
verifier.deployment["uuid"], verifier.status,
|
|
consts.VerifierStatus.INSTALLED))
|
|
|
|
msg = ("Verifier %s has been successfully configured for deployment "
|
|
"'%s' (UUID=%s)!" % (verifier, verifier.deployment["name"],
|
|
verifier.deployment["uuid"]))
|
|
vm = verifier.manager
|
|
if vm.is_configured():
|
|
LOG.info("Verifier is already configured!")
|
|
if not reconfigure:
|
|
if not extra_options:
|
|
return vm.get_configuration()
|
|
else:
|
|
# Just add extra options to the config file.
|
|
if logging.is_debug():
|
|
LOG.debug("Adding the following extra options: %s "
|
|
"to verifier configuration.", extra_options)
|
|
else:
|
|
LOG.info(
|
|
"Adding extra options to verifier configuration.")
|
|
vm.extend_configuration(extra_options)
|
|
LOG.info(msg)
|
|
return vm.get_configuration()
|
|
|
|
LOG.info("Reconfiguring verifier.")
|
|
|
|
raw_config = vm.configure(extra_options=extra_options)
|
|
|
|
LOG.info(msg)
|
|
|
|
return raw_config
|
|
|
|
@classmethod
|
|
def override_configuration(cls, verifier_id, deployment_id,
|
|
new_configuration):
|
|
"""Override verifier configuration (e.g., rewrite the config file).
|
|
|
|
:param verifier_id: Verifier name or UUID
|
|
:param deployment_id: Deployment name or UUID
|
|
:param new_configuration: New configuration for verifier
|
|
"""
|
|
verifier = cls.get(verifier_id)
|
|
if verifier.status != consts.VerifierStatus.INSTALLED:
|
|
raise exceptions.RallyException(
|
|
"Failed to override verifier configuration for deployment "
|
|
"'%s' (UUID=%s) because verifier %s is in '%s' status, but "
|
|
"should be in '%s'." % (
|
|
verifier.deployment["name"], verifier.deployment["uuid"],
|
|
verifier, verifier.status, consts.VerifierStatus.INSTALLED)
|
|
)
|
|
|
|
verifier.set_deployment(deployment_id)
|
|
LOG.info("Overriding configuration of verifier %s for deployment '%s' "
|
|
"(UUID=%s).", verifier, verifier.deployment["name"],
|
|
verifier.deployment["uuid"])
|
|
verifier.manager.override_configuration(new_configuration)
|
|
LOG.info("Configuration of verifier %s has been successfully "
|
|
"overridden for deployment '%s' (UUID=%s)!", verifier,
|
|
verifier.deployment["name"], verifier.deployment["uuid"])
|
|
|
|
@classmethod
|
|
def list_tests(cls, verifier_id, pattern=""):
|
|
"""List all verifier tests.
|
|
|
|
:param verifier_id: Verifier name or UUID
|
|
:param pattern: Pattern which will be used for matching
|
|
"""
|
|
verifier = cls.get(verifier_id)
|
|
if verifier.status != consts.VerifierStatus.INSTALLED:
|
|
raise exceptions.RallyException(
|
|
"Failed to list verifier tests because verifier %s is in '%s' "
|
|
"status, but should be in '%s'." % (
|
|
verifier, verifier.status, consts.VerifierStatus.INSTALLED)
|
|
)
|
|
|
|
if pattern:
|
|
verifier.manager.validate_args({"pattern": pattern})
|
|
|
|
return verifier.manager.list_tests(pattern)
|
|
|
|
@classmethod
|
|
def add_extension(cls, verifier_id, source, version=None,
|
|
extra_settings=None):
|
|
"""Add a verifier extension.
|
|
|
|
:param verifier_id: Verifier name or UUID
|
|
:param source: Path or URL to the repo to clone verifier extension from
|
|
:param version: Branch, tag or commit ID to checkout before
|
|
installation of the verifier extension
|
|
:param extra_settings: Extra installation settings for verifier
|
|
extension
|
|
"""
|
|
verifier = cls.get(verifier_id)
|
|
if verifier.status != consts.VerifierStatus.INSTALLED:
|
|
raise exceptions.RallyException(
|
|
"Failed to add verifier extension because verifier %s "
|
|
"is in '%s' status, but should be in '%s'." % (
|
|
verifier, verifier.status, consts.VerifierStatus.INSTALLED)
|
|
)
|
|
|
|
LOG.info("Adding extension for verifier %s.", verifier)
|
|
|
|
# store original status to rollback it after failure
|
|
original_status = verifier.status
|
|
verifier.update_status(consts.VerifierStatus.EXTENDING)
|
|
try:
|
|
verifier.manager.install_extension(source, version=version,
|
|
extra_settings=extra_settings)
|
|
finally:
|
|
verifier.update_status(original_status)
|
|
|
|
LOG.info("Extension for verifier %s has been successfully added!",
|
|
verifier)
|
|
|
|
@classmethod
|
|
def list_extensions(cls, verifier_id):
|
|
"""List all verifier extensions.
|
|
|
|
:param verifier_id: Verifier name or UUID
|
|
"""
|
|
verifier = cls.get(verifier_id)
|
|
if verifier.status != consts.VerifierStatus.INSTALLED:
|
|
raise exceptions.RallyException(
|
|
"Failed to list verifier extensions because verifier %s "
|
|
"is in '%s' status, but should be in '%s.'" % (
|
|
verifier, verifier.status, consts.VerifierStatus.INSTALLED)
|
|
)
|
|
|
|
return verifier.manager.list_extensions()
|
|
|
|
@classmethod
|
|
def delete_extension(cls, verifier_id, name):
|
|
"""Delete a verifier extension.
|
|
|
|
:param verifier_id: Verifier name or UUID
|
|
:param name: Verifier extension name
|
|
"""
|
|
verifier = cls.get(verifier_id)
|
|
if verifier.status != consts.VerifierStatus.INSTALLED:
|
|
raise exceptions.RallyException(
|
|
"Failed to delete verifier extension because verifier %s "
|
|
"is in '%s' status, but should be in '%s'." % (
|
|
verifier, verifier.status, consts.VerifierStatus.INSTALLED)
|
|
)
|
|
|
|
LOG.info("Deleting extension for verifier %s.", verifier)
|
|
verifier.manager.uninstall_extension(name)
|
|
LOG.info("Extension for verifier %s has been successfully deleted!",
|
|
verifier)
|
|
|
|
|
|
class _Verification(object):
|
|
|
|
@classmethod
|
|
def start(cls, verifier_id, deployment_id, tags=None, **run_args):
|
|
"""Start a verification.
|
|
|
|
:param verifier_id: Verifier name or UUID
|
|
:param deployment_id: Deployment name or UUID
|
|
:param tags: List of tags to assign them to verification
|
|
:param run_args: Dictionary with run arguments for verification
|
|
"""
|
|
# TODO(ylobankov): Add an ability to skip tests by specifying only test
|
|
# names (without test IDs). Also, it would be nice to
|
|
# skip the whole test suites. For example, all tests
|
|
# in the class or module.
|
|
|
|
deployment = objects.Deployment.get(deployment_id)
|
|
|
|
if deployment["status"] != consts.DeployStatus.DEPLOY_FINISHED:
|
|
raise exceptions.DeploymentNotFinishedStatus(
|
|
name=deployment["name"],
|
|
uuid=deployment["uuid"],
|
|
status=deployment["status"])
|
|
|
|
verifier = _Verifier.get(verifier_id)
|
|
if verifier.status != consts.VerifierStatus.INSTALLED:
|
|
raise exceptions.RallyException(
|
|
"Failed to start verification because verifier %s is in '%s' "
|
|
"status, but should be in '%s'." % (
|
|
verifier, verifier.status, consts.VerifierStatus.INSTALLED)
|
|
)
|
|
|
|
verifier.set_deployment(deployment_id)
|
|
if not verifier.manager.is_configured():
|
|
_Verifier.configure(verifier, deployment_id)
|
|
|
|
# TODO(andreykurilin): save validation results to db
|
|
verifier.manager.validate(run_args)
|
|
|
|
verification = objects.Verification.create(
|
|
verifier_id=verifier_id, deployment_id=deployment_id, tags=tags,
|
|
run_args=run_args)
|
|
LOG.info("Starting verification (UUID=%s) for deployment '%s' "
|
|
"(UUID=%s) by verifier %s.", verification.uuid,
|
|
verifier.deployment["name"], verifier.deployment["uuid"],
|
|
verifier)
|
|
verification.update_status(consts.VerificationStatus.RUNNING)
|
|
|
|
context = {"config": verifier.manager._meta_get("context"),
|
|
"run_args": run_args,
|
|
"verification": verification,
|
|
"verifier": verifier}
|
|
try:
|
|
with vcontext.ContextManager(context):
|
|
results = verifier.manager.run(context)
|
|
except Exception as e:
|
|
verification.set_error(e)
|
|
raise
|
|
|
|
# TODO(ylobankov): Check that verification exists in the database
|
|
# because users may delete verification before tests
|
|
# finish.
|
|
verification.finish(results.totals, results.tests)
|
|
|
|
LOG.info("Verification (UUID=%s) has been successfully finished for "
|
|
"deployment '%s' (UUID=%s)!", verification.uuid,
|
|
verifier.deployment["name"], verifier.deployment["uuid"])
|
|
|
|
return verification, results
|
|
|
|
@classmethod
|
|
def rerun(cls, verification_uuid, deployment_id=None, failed=False):
|
|
"""Rerun tests from a verification.
|
|
|
|
:param verification_uuid: Verification UUID
|
|
:param deployment_id: Deployment name or UUID
|
|
:param failed: Rerun only failed tests
|
|
"""
|
|
# TODO(ylobankov): Improve this method in the future: put some
|
|
# information about re-run in run_args.
|
|
|
|
verification = cls.get(verification_uuid)
|
|
tests = verification.tests
|
|
|
|
if failed:
|
|
tests = [t for t, r in tests.items() if r["status"] == "fail"]
|
|
if not tests:
|
|
raise exceptions.RallyException(
|
|
"There are no failed tests from verification (UUID=%s)."
|
|
% verification_uuid)
|
|
else:
|
|
tests = tests.keys()
|
|
|
|
deployment = _Deployment.get(deployment_id or
|
|
verification.deployment_uuid)
|
|
LOG.info("Re-running %stests from verification (UUID=%s) for "
|
|
"deployment '%s' (UUID=%s).", "failed " if failed else "",
|
|
verification.uuid, deployment["name"], deployment["uuid"])
|
|
return cls.start(verification.verifier_uuid, deployment["uuid"],
|
|
load_list=tests)
|
|
|
|
@staticmethod
|
|
def get(verification_uuid):
|
|
"""Get a verification.
|
|
|
|
:param verification_uuid: Verification UUID
|
|
"""
|
|
return objects.Verification.get(verification_uuid)
|
|
|
|
@staticmethod
|
|
def list(verifier_id=None, deployment_id=None, tags=None, status=None):
|
|
"""List all verifications.
|
|
|
|
:param verifier_id: Verifier name or UUID
|
|
:param deployment_id: Deployment name or UUID
|
|
:param tags: Tags to filter verifications by
|
|
:param status: Status to filter verifications by
|
|
"""
|
|
return objects.Verification.list(verifier_id,
|
|
deployment_id=deployment_id,
|
|
tags=tags, status=status)
|
|
|
|
@classmethod
|
|
def delete(cls, verification_uuid):
|
|
"""Delete a verification.
|
|
|
|
:param verification_uuid: Verification UUID
|
|
"""
|
|
verification = cls.get(verification_uuid)
|
|
LOG.info("Deleting verification (UUID=%s).", verification.uuid)
|
|
verification.delete()
|
|
LOG.info("Verification has been successfully deleted!")
|
|
|
|
@classmethod
|
|
def report(cls, uuids, output_type, output_dest=None):
|
|
"""Generate a report for a verification or a few verifications.
|
|
|
|
:param uuids: List of verifications UUIDs
|
|
:param output_type: Plugin name of verification reporter
|
|
:param output_dest: Destination for verification report
|
|
"""
|
|
verifications = [cls.get(uuid) for uuid in uuids]
|
|
|
|
reporter_cls = vreporter.VerificationReporter.get(output_type)
|
|
reporter_cls.validate(output_dest)
|
|
|
|
LOG.info("Building '%s' report for the following verification(s): "
|
|
"'%s'.", output_type, "', '".join(uuids))
|
|
result = vreporter.VerificationReporter.make(reporter_cls,
|
|
verifications,
|
|
output_dest)
|
|
LOG.info(_LI("The report has been successfully built."))
|
|
return result
|
|
|
|
@classmethod
|
|
def import_results(cls, verifier_id, deployment_id, data, **run_args):
|
|
"""Import results of a test run into Rally database.
|
|
|
|
:param verifier_id: Verifier name or UUID
|
|
:param deployment_id: Deployment name or UUID
|
|
:param data: Results data of a test run to import
|
|
:param run_args: Dictionary with run arguments
|
|
"""
|
|
# TODO(aplanas): Create an external deployment if this is missing, as
|
|
# required in the blueprint [1].
|
|
# [1] https://blueprints.launchpad.net/rally/+spec/verification-import
|
|
|
|
verifier = _Verifier.get(verifier_id)
|
|
verifier.set_deployment(deployment_id)
|
|
LOG.info("Importing test results into a new verification for "
|
|
"deployment '%s' (UUID=%s), using verifier %s.",
|
|
verifier.deployment["name"], verifier.deployment["uuid"],
|
|
verifier)
|
|
|
|
verifier.manager.validate_args(run_args)
|
|
|
|
verification = objects.Verification.create(verifier_id,
|
|
deployment_id=deployment_id,
|
|
run_args=run_args)
|
|
verification.update_status(consts.VerificationStatus.RUNNING)
|
|
|
|
try:
|
|
results = verifier.manager.parse_results(data)
|
|
except Exception as e:
|
|
verification.set_failed(e)
|
|
raise
|
|
verification.finish(results.totals, results.tests)
|
|
|
|
LOG.info("Test results have been successfully imported.")
|
|
|
|
return verification, results
|
|
|
|
|
|
class _DeprecatedAPIClass(object):
|
|
"""Deprecates direct usage of api classes."""
|
|
def __init__(self, cls):
|
|
self._cls = cls
|
|
|
|
def __getattr__(self, attr, default=None):
|
|
LOG.warning(_LW("'%s' is deprecated since Rally 0.8.0 in favor of "
|
|
"'rally.api.API' class.") % self._cls.__name__[1:])
|
|
return getattr(self._cls, attr, default)
|
|
|
|
|
|
Deployment = _DeprecatedAPIClass(_Deployment)
|
|
Task = _DeprecatedAPIClass(_Task)
|
|
|
|
|
|
class API(object):
|
|
|
|
CONFIG_SEARCH_PATHS = [sys.prefix + "/etc/rally", "~/.rally", "/etc/rally"]
|
|
CONFIG_FILE_NAME = "rally.conf"
|
|
|
|
def __init__(self, config_file=None, config_args=None,
|
|
rally_endpoint=None, plugin_paths=None, skip_db_check=False):
|
|
"""Initialize Rally API instance
|
|
|
|
:param config_file: Path to rally configuration file. If None, default
|
|
path will be selected
|
|
:type config_file: str
|
|
:param config_args: Arguments for initialization current configuration
|
|
:type config_args: list
|
|
:param rally_endpoint: [Restricted]Rally endpoint connection string.
|
|
:type rally_endpoint: str
|
|
:param plugin_paths: Additional custom plugin locations
|
|
:type plugin_paths: list
|
|
:param skip_db_check: Allows to skip db revision check
|
|
:type skip_db_check: bool
|
|
"""
|
|
if rally_endpoint:
|
|
raise NotImplementedError(_LE("Sorry, but Rally-as-a-Service is "
|
|
"not ready yet."))
|
|
try:
|
|
config_files = ([config_file] if config_file else
|
|
self._default_config_file())
|
|
CONF(config_args or [],
|
|
project="rally",
|
|
version=rally_version.version_string(),
|
|
default_config_files=config_files)
|
|
logging.setup("rally")
|
|
if not CONF.get("log_config_append"):
|
|
# The below two lines are to disable noise from request module.
|
|
# The standard way should be we make such lots of settings on
|
|
# the root rally. However current oslo codes doesn't support
|
|
# such interface. So I choose to use a 'hacking' way to avoid
|
|
# INFO logs from request module where user didn't give specific
|
|
# log configuration. And we could remove this hacking after
|
|
# oslo.log has such interface.
|
|
LOG.debug(
|
|
"INFO logs from urllib3 and requests module are hide.")
|
|
requests_log = logging.getLogger("requests").logger
|
|
requests_log.setLevel(logging.WARNING)
|
|
urllib3_log = logging.getLogger("urllib3").logger
|
|
urllib3_log.setLevel(logging.WARNING)
|
|
|
|
LOG.debug("urllib3 insecure warnings are hidden.")
|
|
for warning in ("InsecurePlatformWarning",
|
|
"SNIMissingWarning",
|
|
"InsecureRequestWarning"):
|
|
warning_cls = getattr(urllib3.exceptions, warning, None)
|
|
if warning_cls is not None:
|
|
urllib3.disable_warnings(warning_cls)
|
|
|
|
# NOTE(wtakase): This is for suppressing boto error logging.
|
|
LOG.debug("ERROR log from boto module is hide.")
|
|
boto_log = logging.getLogger("boto").logger
|
|
boto_log.setLevel(logging.CRITICAL)
|
|
|
|
# Set alembic log level to ERROR
|
|
alembic_log = logging.getLogger("alembic").logger
|
|
alembic_log.setLevel(logging.ERROR)
|
|
|
|
except cfg.ConfigFilesNotFoundError as e:
|
|
cfg_files = e.config_files
|
|
raise exceptions.RallyException(_LE(
|
|
"Failed to read configuration file(s): %s") % cfg_files)
|
|
|
|
# Check that db is upgraded to the latest revision
|
|
if not skip_db_check:
|
|
self.check_db_revision()
|
|
|
|
# Load plugins
|
|
plugin_paths = plugin_paths or []
|
|
if "plugin_paths" in CONF:
|
|
plugin_paths.extend(CONF.get("plugin_paths") or [])
|
|
for path in plugin_paths:
|
|
discover.load_plugins(path)
|
|
|
|
# NOTE(andreykurilin): There is no reason to auto-discover API's. We
|
|
# have only 4 classes, so let's do it in good old way - hardcode them:)
|
|
self._deployment = _Deployment
|
|
self._task = _Task
|
|
self._verifier = _Verifier
|
|
self._verification = _Verification
|
|
|
|
def _default_config_file(self):
|
|
for path in self.CONFIG_SEARCH_PATHS:
|
|
abspath = os.path.abspath(os.path.expanduser(path))
|
|
fpath = os.path.join(abspath, self.CONFIG_FILE_NAME)
|
|
if os.path.isfile(fpath):
|
|
return [fpath]
|
|
|
|
def check_db_revision(self):
|
|
rev = rally_version.database_revision()
|
|
|
|
# Check that db exists
|
|
if rev["revision"] is None:
|
|
raise exceptions.RallyException(_LE(
|
|
"Database is missing. Create database by command "
|
|
"`rally-manage db create'"))
|
|
|
|
# Check that db is updated
|
|
if rev["revision"] != rev["current_head"]:
|
|
raise exceptions.RallyException(_LE(
|
|
"Database seems to be outdated. Run upgrade from "
|
|
"revision %(revision)s to %(current_head)s by command "
|
|
"`rally-manage db upgrade'") % rev)
|
|
|
|
@property
|
|
def deployment(self):
|
|
return self._deployment
|
|
|
|
@property
|
|
def task(self):
|
|
return self._task
|
|
|
|
@property
|
|
def verifier(self):
|
|
return self._verifier
|
|
|
|
@property
|
|
def verification(self):
|
|
return self._verification
|