614 lines
24 KiB
Python
614 lines
24 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 time
|
|
|
|
import jinja2
|
|
import jinja2.meta
|
|
import jsonschema
|
|
|
|
from rally.common.i18n import _, _LI, _LE
|
|
from rally.common import logging
|
|
from rally.common import objects
|
|
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.tempest import tempest
|
|
|
|
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)
|
|
tempest.Tempest(deployment["uuid"]).uninstall()
|
|
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"])
|
|
|
|
deployment.delete()
|
|
|
|
@classmethod
|
|
def recreate(cls, deployment):
|
|
"""Performs a cleanup and then makes a deployment again.
|
|
|
|
:param deployment: UUID or name of the deployment
|
|
"""
|
|
deployment = objects.Deployment.get(deployment)
|
|
deployer = deploy_engine.Engine.get_engine(
|
|
deployment["config"]["type"], deployment)
|
|
with deployer:
|
|
deployer.make_cleanup()
|
|
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(kun): put this work into objects.Deployment
|
|
clients = osclients.Clients(objects.Credential(**deployment["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
|
|
"""
|
|
services = cls.service_list(deployment)
|
|
users = deployment["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_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
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, admin=deployment["admin"], users=deployment["users"])
|
|
|
|
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, admin=deployment["admin"], users=deployment["users"],
|
|
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.FAILED]
|
|
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
|
|
FINISHED and the force argument
|
|
is not True
|
|
"""
|
|
status = None if force else consts.TaskStatus.FINISHED
|
|
objects.Task.delete_by_uuid(task_uuid, status=status)
|
|
|
|
|
|
class Verification(object):
|
|
|
|
@staticmethod
|
|
def _check_tempest_tree_existence(verifier):
|
|
if not os.path.exists(verifier.path()):
|
|
msg = _("Tempest tree for "
|
|
"deployment '%s' not found! ") % verifier.deployment
|
|
LOG.error(
|
|
msg + _("Use `rally verify install` for Tempest installation"))
|
|
raise exceptions.NotFoundException(message=msg)
|
|
|
|
@classmethod
|
|
def verify(cls, deployment, set_name="", regex=None, tests_file=None,
|
|
tests_file_to_skip=None, tempest_config=None,
|
|
expected_failures=None, system_wide=False, concur=0,
|
|
failing=False):
|
|
"""Start verification.
|
|
|
|
:param deployment: UUID or name of a deployment
|
|
:param set_name: Name of a Tempest test set
|
|
:param regex: Regular expression of test
|
|
:param tests_file: Path to a file with a list of Tempest tests
|
|
to run them
|
|
:param tests_file_to_skip: Path to a file with a list of Tempest tests
|
|
to skip them
|
|
:param tempest_config: User specified Tempest config file location
|
|
:param expected_failures: Dictionary with Tempest tests that are
|
|
expected to fail. Keys are test names;
|
|
values are reasons of test failures
|
|
:param system_wide: Whether or not to create a virtual env when
|
|
installing Tempest; whether or not to use
|
|
the local env instead of the Tempest virtual
|
|
env when running the tests
|
|
:param concur: How many processes to use to run Tempest tests.
|
|
The default value (0) auto-detects CPU count
|
|
:param failing: Re-run tests that failed during the last execution
|
|
:returns: Verification object
|
|
"""
|
|
|
|
deployment_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
verification = objects.Verification(deployment_uuid=deployment_uuid)
|
|
verifier = tempest.Tempest(deployment_uuid,
|
|
verification=verification,
|
|
tempest_config=tempest_config,
|
|
system_wide=system_wide)
|
|
|
|
cls._check_tempest_tree_existence(verifier)
|
|
|
|
LOG.info("Starting verification of deployment: %s" % deployment_uuid)
|
|
verification.set_running()
|
|
verifier.verify(set_name=set_name, regex=regex, tests_file=tests_file,
|
|
tests_file_to_skip=tests_file_to_skip,
|
|
expected_failures=expected_failures, concur=concur,
|
|
failing=failing)
|
|
|
|
return verification
|
|
|
|
@classmethod
|
|
def import_results(cls, deployment, set_name="", log_file=None):
|
|
"""Import Tempest tests results into the Rally database.
|
|
|
|
:param deployment: UUID or name of a deployment
|
|
:param log_file: User specified Tempest log file in subunit format
|
|
:returns: Deployment and verification objects
|
|
"""
|
|
# 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
|
|
deployment_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
verification = objects.Verification(deployment_uuid=deployment_uuid)
|
|
verifier = tempest.Tempest(deployment_uuid, verification=verification)
|
|
|
|
LOG.info("Importing verification of deployment: %s" % deployment_uuid)
|
|
verification.set_running()
|
|
verifier.import_results(set_name=set_name, log_file=log_file)
|
|
|
|
return deployment, verification
|
|
|
|
@classmethod
|
|
def install_tempest(cls, deployment, source=None, version=None,
|
|
system_wide=False):
|
|
"""Install Tempest.
|
|
|
|
:param deployment: UUID or name of a deployment
|
|
:param source: Path/URL to repo to clone Tempest from
|
|
:param version: Commit ID or tag to checkout before Tempest
|
|
installation
|
|
:param system_wide: Whether or not to install Tempest package and
|
|
create a Tempest virtual env
|
|
"""
|
|
deployment_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
verifier = tempest.Tempest(deployment_uuid, source=source,
|
|
version=version, system_wide=system_wide)
|
|
verifier.install()
|
|
|
|
@classmethod
|
|
def uninstall_tempest(cls, deployment):
|
|
"""Remove deployment's local Tempest installation.
|
|
|
|
:param deployment: UUID or name of a deployment
|
|
"""
|
|
deployment_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
verifier = tempest.Tempest(deployment_uuid)
|
|
verifier.uninstall()
|
|
|
|
@classmethod
|
|
def reinstall_tempest(cls, deployment, source=None, version=None,
|
|
system_wide=False):
|
|
"""Uninstall Tempest and install again.
|
|
|
|
:param deployment: UUID or name of a deployment
|
|
:param source: Path/URL to repo to clone Tempest from
|
|
:param version: Commit ID or tag to checkout before Tempest
|
|
installation
|
|
:param system_wide: Whether or not to install Tempest package and
|
|
create a Tempest virtual env
|
|
"""
|
|
deployment_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
verifier = tempest.Tempest(deployment_uuid, source=source,
|
|
version=version, system_wide=system_wide)
|
|
verifier.uninstall()
|
|
verifier.install()
|
|
|
|
@classmethod
|
|
def install_tempest_plugin(cls, deployment, source=None, version=None,
|
|
system_wide=False):
|
|
"""Install Tempest plugin.
|
|
|
|
:param deployment: UUID or name of a deployment
|
|
:param source: Path/URL to repo to clone Tempest plugin from
|
|
:param version: Branch, commit ID or tag to checkout before Tempest
|
|
plugin installation
|
|
:param system_wide: Install plugin in Tempest virtual env or
|
|
in the local env
|
|
"""
|
|
deployment_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
verifier = tempest.Tempest(deployment_uuid,
|
|
plugin_source=source,
|
|
plugin_version=version,
|
|
system_wide=system_wide)
|
|
|
|
cls._check_tempest_tree_existence(verifier)
|
|
|
|
verifier.install_plugin()
|
|
|
|
@classmethod
|
|
def list_tempest_plugins(cls, deployment, system_wide=False):
|
|
"""List all installed Tempest plugins.
|
|
|
|
:param deployment: UUID or name of a deployment
|
|
:param system_wide: List all plugins installed in the local env or
|
|
in Tempest virtual env
|
|
"""
|
|
deployment_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
verifier = tempest.Tempest(deployment_uuid, system_wide=system_wide)
|
|
|
|
cls._check_tempest_tree_existence(verifier)
|
|
|
|
return verifier.list_plugins()
|
|
|
|
@classmethod
|
|
def uninstall_tempest_plugin(cls, deployment, repo_name,
|
|
system_wide=False):
|
|
"""Uninstall Tempest plugin.
|
|
|
|
:param deployment: UUID or name of a deployment
|
|
:param repo_name: Plugin repo name
|
|
:param system_wide: Uninstall plugin from Tempest virtual env or
|
|
from the local env
|
|
"""
|
|
deployment_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
verifier = tempest.Tempest(deployment_uuid, system_wide=system_wide)
|
|
verifier.uninstall_plugin(repo_name)
|
|
|
|
@classmethod
|
|
def discover_tests(cls, deployment, pattern="", system_wide=False):
|
|
"""Get a list of discovered tests.
|
|
|
|
:param deployment: UUID or name of a deployment
|
|
:param pattern: Test name pattern which can be used to match
|
|
:param system_wide: Discover tests for system-wide or venv
|
|
Tempest installation
|
|
"""
|
|
deployment_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
verifier = tempest.Tempest(deployment_uuid, system_wide=system_wide)
|
|
|
|
cls._check_tempest_tree_existence(verifier)
|
|
|
|
return verifier.discover_tests(pattern)
|
|
|
|
@classmethod
|
|
def configure_tempest(cls, deployment, tempest_config=None,
|
|
extra_conf=None, override=False):
|
|
"""Generate Tempest configuration file.
|
|
|
|
:param deployment: UUID or name of a deployment
|
|
:param tempest_config: User specified Tempest config file location
|
|
:param extra_conf: A ConfigParser() object with options to
|
|
extend/update Tempest config file
|
|
:param override: Whether or not to override existing Tempest
|
|
config file
|
|
"""
|
|
deployment_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
verifier = tempest.Tempest(deployment_uuid,
|
|
tempest_config=tempest_config)
|
|
|
|
cls._check_tempest_tree_existence(verifier)
|
|
|
|
verifier.generate_config_file(extra_conf, override)
|
|
|
|
@classmethod
|
|
def show_config_info(cls, deployment):
|
|
"""Get information about Tempest configuration file.
|
|
|
|
:param deployment: UUID or name of a deployment
|
|
"""
|
|
deployment_uuid = objects.Deployment.get(deployment)["uuid"]
|
|
verifier = tempest.Tempest(deployment_uuid)
|
|
|
|
cls._check_tempest_tree_existence(verifier)
|
|
|
|
if not verifier.is_configured():
|
|
verifier.generate_config_file()
|
|
|
|
with open(verifier.config_file, "rb") as conf:
|
|
return {"conf_data": conf.read(),
|
|
"conf_path": verifier.config_file}
|
|
|
|
@staticmethod
|
|
def list(status=None):
|
|
"""List all verifications.
|
|
|
|
:param status: Filter verifications by the specified status
|
|
"""
|
|
return objects.Verification.list(status)
|
|
|
|
@staticmethod
|
|
def get(verification_uuid):
|
|
"""Get verification.
|
|
|
|
:param verification_uuid: UUID of a verification
|
|
"""
|
|
return objects.Verification.get(verification_uuid)
|