[envs] Introduce Env Manager & Platforms (part 1)

EnvManager is one of key Rally components,
It is actually replacment for Rally deployment component.
It manages and stores information about tested platforms.
Every Env has:
    - unique name and UUID
    - dates when it was created and updated
    - plugins spec and data
    - platforms names and data

Comparing to Rally deployment, env manager is way simpler and flat.
EnvManager manage Platforms (which are plugins)

However, even if it is simpler it allows to do way more flexible things
like manage multiple platforms in single env.

This patch:
- Introduce EnvManager
- Intorudce Platform plugin base
- Intorduce DB layer for Envs & Platforms
- Intrdouce 3 Exceptions:
-- ManagerException
---- ManagerInvalidSpec
---- ManagerInvalidState

Change-Id: Ide95c8b1e8e72293c009f4cd5e430e64eb1dd604
This commit is contained in:
Boris Pavlovic 2017-10-13 16:31:07 -07:00
parent c4dd2d6e56
commit 4719c5d8c7
13 changed files with 1960 additions and 6 deletions

View File

@ -362,6 +362,67 @@ def resource_delete(id):
return get_impl().resource_delete(id)
def env_get(uuid_or_name):
"""Returns envs with corresponding uuid or name."""
return get_impl().env_get(uuid_or_name)
def env_get_status(uuid):
"""Returns status of env with corresponding uuid."""
return get_impl().env_get_status(uuid)
def env_list(status=None):
"""Return list of envs, filtered by status, if status provided."""
return get_impl().env_list(status=status)
def env_create(name, status, description, extras, spec, platforms):
"""Created db record of env and platforms."""
return get_impl().env_create(
name, status, description, extras, spec, platforms)
def env_rename(uuid, old_name, new_name):
"""Renames env. Returns op result as bool"""
return get_impl().env_rename(uuid, old_name, new_name)
def env_update(uuid, description=None, extras=None):
"""Update description and extra of envs. Returns op result as bool."""
return get_impl().env_update(uuid, description=description, extras=extras)
def env_set_status(uuid, old_status, new_status):
"""Set new env status. """
return get_impl().env_set_status(uuid, old_status, new_status)
def env_delete_cascade(uuid):
"""Delete envs, platforms and all related to env resources."""
return get_impl().env_delete_cascade(uuid)
def platforms_list(env_uuid):
"""List platforms related to some env."""
return get_impl().platforms_list(env_uuid)
def platform_get(uuid):
"""Returns platforms with corresponding uuid."""
return get_impl().platform_get(uuid)
def platform_set_status(uuid, old_status, new_status):
"""Set's new status to platform"""
return get_impl().platform_set_status(uuid, old_status, new_status)
def platform_set_data(uuid, platform_data=None, plugin_data=None):
"""Set's platform data."""
return get_impl().platform_set_data(uuid, platform_data, plugin_data)
def verifier_create(name, vtype, platform, source, version, system_wide,
extra_settings=None):
"""Create a verifier record.

View File

@ -206,9 +206,6 @@ class Connection(object):
:raises Exception: when the model is not a sublcass of
:class:`rally.common.db.sqlalchemy.models.RallyBase`.
"""
session = session or get_session()
query = session.query(model)
def issubclassof_rally_base(obj):
return isinstance(obj, type) and issubclass(obj, models.RallyBase)
@ -216,7 +213,8 @@ class Connection(object):
raise exceptions.DBException(
"The model %s should be a subclass of RallyBase" % model)
return query
session = session or get_session()
return session.query(model)
def _tags_get(self, uuid, tag_type, session=None):
tags = (self.model_query(models.Tag, session=session).
@ -571,6 +569,143 @@ class Connection(object):
session.query(models.Subtask).filter_by(uuid=subtask_uuid).update(
subtask_values)
@serialize
def env_get(self, uuid_or_name):
env = (self.model_query(models.Env)
.filter(sa.or_(models.Env.uuid == uuid_or_name,
models.Env.name == uuid_or_name))
.first())
if not env:
raise exceptions.DBRecordNotFound(
criteria="uuid or name is %s" % uuid_or_name, table="envs")
return env
@serialize
def env_get_status(self, uuid):
resp = (self.model_query(models.Env)
.filter_by(uuid=uuid)
.options(sa.orm.load_only("status"))
.first())
if not resp:
raise exceptions.DBRecordNotFound(
criteria="uuid: %s" % uuid, table="envs")
return resp["status"]
@serialize
def env_list(self, status=None):
query = self.model_query(models.Env)
if status:
query = query.filter_by(status=status)
return query.all()
@serialize
def env_create(self, name, status, description, extras, spec, platforms):
try:
env_uuid = models.UUID()
for p in platforms:
p["env_uuid"] = env_uuid
env = models.Env(
name=name, uuid=env_uuid,
status=status, description=description,
extras=extras, spec=spec
)
# db_session.add(env)
get_session().bulk_save_objects([env] + [
models.Platform(**p) for p in platforms
])
except db_exc.DBDuplicateEntry:
raise exceptions.DBRecordExists(
field="name", value=name, table="envs")
return self.env_get(env_uuid)
def env_rename(self, uuid, old_name, new_name):
try:
return bool(self.model_query(models.Env)
.filter_by(uuid=uuid, name=old_name)
.update({"name": new_name}))
except db_exc.DBDuplicateEntry:
raise exceptions.DBRecordExists(
field="name", value=new_name, table="envs")
def env_update(self, uuid, description=None, extras=None):
values = {}
if description is not None:
values["description"] = description
if extras is not None:
values["extras"] = extras
if not values:
return True
return bool(self.model_query(models.Env)
.filter_by(uuid=uuid)
.update(values))
def env_set_status(self, uuid, old_status, new_status):
count = (self.model_query(models.Env)
.filter_by(uuid=uuid, status=old_status)
.update({"status": new_status}))
if count:
return True
raise exceptions.DBConflict(
"Env %s should be in status %s actual %s"
% (uuid, old_status, self.env_get_status(uuid)))
def env_delete_cascade(self, uuid):
session = get_session()
with session.begin():
(self.model_query(models.Platform, session=session)
.filter_by(env_uuid=uuid)
.delete())
(self.model_query(models.Env, session=session)
.filter_by(uuid=uuid)
.delete())
# NOTE(boris-42): Add queries to delete corresponding
# task and verify results, when they switch to Env
@serialize
def platforms_list(self, env_uuid):
return (self.model_query(models.Platform)
.filter_by(env_uuid=env_uuid)
.all())
@serialize
def platform_get(self, uuid):
p = self.model_query(models.Platform).filter_by(uuid=uuid).first()
if not p:
raise exceptions.DBRecordNotFound(
criteria="uuid = %s" % uuid, table="platforms")
return p
def platform_set_status(self, uuid, old_status, new_status):
count = (self.model_query(models.Platform)
.filter_by(uuid=uuid, status=old_status)
.update({"status": new_status}))
if count:
return True
platform = self.platform_get(uuid)
raise exceptions.DBConflict(
"Platform %s should be in status %s actual %s"
% (uuid, old_status, platform["status"]))
def platform_set_data(self, uuid, platform_data=None, plugin_data=None):
values = {}
if platform_data is not None:
values["platform_data"] = platform_data
if plugin_data is not None:
values["plugin_data"] = plugin_data
if not values:
return True
return bool(self.model_query(models.Platform)
.filter_by(uuid=uuid)
.update(values))
def _deployment_get(self, deployment, session=None):
stored_deployment = self.model_query(
models.Deployment,

View File

@ -0,0 +1,86 @@
# 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.
"""Add Env & Platforms tables
Revision ID: a43700a813a5
Revises: dc46687661df
Create Date: 2017-12-27 13:37:10.144970
"""
from alembic import op
import sqlalchemy as sa
from rally.common.db.sqlalchemy import types as sa_types
from rally import exceptions
# revision identifiers, used by Alembic.
revision = "a43700a813a5"
down_revision = "44169f4d455e"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"envs",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("uuid", sa.String(36), nullable=False),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("description", sa.Text, default=""),
sa.Column("status", sa.String(36), nullable=False),
sa.Column("extras", sa_types.MutableJSONEncodedDict, default={}),
sa.Column("spec", sa_types.MutableJSONEncodedDict, default={}),
sa.Column("created_at", sa.DateTime),
sa.Column("updated_at", sa.DateTime)
)
op.create_index("env_uuid", "envs", ["uuid"], unique=True)
op.create_index("env_name", "envs", ["name"], unique=True)
op.create_index("env_status", "envs", ["status"])
op.create_table(
"platforms",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("uuid", sa.String(36), nullable=False),
sa.Column("env_uuid", sa.String(36), nullable=False),
sa.Column("status", sa.String(36), nullable=False),
sa.Column("plugin_name", sa.String(36), nullable=False),
sa.Column("plugin_spec", sa_types.MutableJSONEncodedDict,
nullable=False),
sa.Column("plugin_data", sa_types.MutableJSONEncodedDict,
default={}),
sa.Column("platform_name", sa.String(36)),
sa.Column("platform_data", sa_types.MutableJSONEncodedDict,
default={}),
sa.Column("created_at", sa.DateTime),
sa.Column("updated_at", sa.DateTime),
)
op.create_index("platform_uuid", "platforms", ["uuid"], unique=True)
op.create_index("platform_env_uuid", "platforms", ["env_uuid"])
def downgrade():
raise exceptions.DowngradeNotSupported()

View File

@ -136,6 +136,47 @@ class Resource(BASE, RallyBase):
)
class Env(BASE, RallyBase):
"""Represent a environment."""
__tablename__ = "envs"
__table_args__ = (
sa.Index("env_uuid", "uuid", unique=True),
sa.Index("env_name", "name", unique=True),
sa.Index("env_status", "status")
)
id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
uuid = sa.Column(sa.String(36), default=UUID, nullable=False)
name = sa.Column(sa.String(255), nullable=False)
description = sa.Column(sa.Text, default="")
status = sa.Column(sa.String(36), nullable=False)
extras = sa.Column(sa_types.MutableJSONEncodedDict, default={})
spec = sa.Column(sa_types.MutableJSONEncodedDict, default={})
class Platform(BASE, RallyBase):
"""Represent environment's platforms."""
__tablename__ = "platforms"
__table_args__ = (
sa.Index("platform_uuid", "uuid", unique=True),
sa.Index("platform_env_uuid", "env_uuid")
)
id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
uuid = sa.Column(sa.String(36), default=UUID, nullable=False)
env_uuid = sa.Column(sa.String(36), nullable=False)
status = sa.Column(sa.String(36), nullable=False)
plugin_name = sa.Column(sa.String(36), nullable=False)
plugin_spec = sa.Column(sa_types.MutableJSONEncodedDict, default={},
nullable=False)
plugin_data = sa.Column(sa_types.MutableJSONEncodedDict, default={})
platform_name = sa.Column(sa.String(36))
platform_data = sa.Column(sa_types.MutableJSONEncodedDict, default={})
class Task(BASE, RallyBase):
"""Represents a task."""
__tablename__ = "tasks"

0
rally/env/__init__.py vendored Normal file
View File

578
rally/env/env_mgr.py vendored Normal file
View File

@ -0,0 +1,578 @@
# 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 copy
import sys
import jsonschema
from rally.common import db
from rally.common import logging
from rally.common import utils
from rally.env import platform
from rally import exceptions
LOG = logging.getLogger(__name__)
class _EnvStatus(utils.ImmutableMixin, utils.EnumMixin):
"""Rally Env Statuses."""
INIT = "INITIALIZING"
READY = "READY"
FAILED_TO_CREATE = "FAILED TO CREATE"
CLEANING = "CLEANING"
DESTROYING = "DESTROYING"
FAILED_TO_DESTROY = "FAILED TO DESTROY"
DESTROYED = "DESTROYED"
TRANSITION_TABLE = {
INIT: (READY, FAILED_TO_CREATE),
READY: (DESTROYING, CLEANING),
CLEANING: (READY, ),
FAILED_TO_CREATE: (DESTROYING, ),
DESTROYING: (DESTROYED, FAILED_TO_DESTROY),
FAILED_TO_DESTROY: (DESTROYING, )
}
STATUS = _EnvStatus()
class EnvManager(object):
"""Implements life cycle management of Rally Envs.
EnvManager is one of key Rally components,
It manages and stores information about tested platforms. Every Env has:
- unique name and UUID
- dates when it was created and updated
- platform plugins spec and data
- platform data
Env Input has next syntax:
{
"type": "object",
"properties": {
"version": {
"type": "integer",
"description": "Env version"
},
"description": {
"type": "string",
"description": "User specific description of deployment"
},
"extras": {
"type": "object",
"description": "Custom dict with data"
},
"config": {
"type": "object",
"properties": {
"*": {
"type": "object",
"*": {},
"description": |
Keys are option's name, values are option's values
},
"description": "Keys are groups, values are options names"
}
},
"platforms": {
"type": "object",
"properties": {
"*": {
"type": "object",
"description": |
Key is platform plugin name,
values are plugin arguments
}
}
}
}
}
Env.data property is dict that is consumed by other rally components,
like task, verify and maybe something else in future.
{
"name": {"type": "string"},
"status": {"type": "string"},
"description: {"type": "string"},
"extras": {"type": "object"},
"config": {"type": "object"},
"platforms": {
"type": "object",
"*": {
"type": "object"
"description": "Key is platform name, value is platform data"
}
}
}
"""
def __init__(self, _data):
"""Private method to initializes env manager.
THis method is not meant to be called directly, use one of
next class methods: get(), create() or list().
"""
self._env = _data
self.uuid = self._env["uuid"]
@property
def status(self):
"""Returns current state of Env that was fetched from DB."""
return db.env_get_status(self.uuid)
@property
def data(self):
"""Returns full information about env including platforms."""
self._env = db.env_get(self.uuid)
return {
"id": self._env["id"],
"uuid": self._env["uuid"],
"created_at": self._env["created_at"],
"updated_at": self._env["updated_at"],
"name": self._env["name"],
"description": self._env["description"],
"status": self._env["status"],
"spec": copy.deepcopy(self._env["spec"]),
"extras": copy.deepcopy(self._env["extras"]),
"platforms": db.platforms_list(self.uuid)
}
def _get_platforms(self):
"""Iterate over Envs platforms.
:returns: Generator that returns list of tuples
(uuid, instance of rally.env.platform.Platform)
"""
raw_platforms = db.platforms_list(self.uuid)
platforms = []
for p in raw_platforms:
plugin_cls = platform.Platform.get(p["plugin_name"])
platforms.append(
plugin_cls(
p["plugin_spec"],
uuid=p["uuid"],
plugin_data=p["plugin_data"],
platform_data=p["platform_data"],
status=p["status"]
)
)
return platforms
@classmethod
def get(cls, uuid_or_name):
"""Get the instance of EnvManager by uuid or name.
:param uuid_or_name: Returns record that has uuid or name equal to it.
:returns: Instance of rally.env.env_mgr.EnvManager
"""
return cls(db.env_get(uuid_or_name))
@classmethod
def list(cls, status=None):
"""Returns list of instances of EnvManagers."""
return [cls(data) for data in db.env_list(status=status)]
@classmethod
def _validate_and_create_env(cls, name, description, extras, spec):
"""Validated and create env and platforms DB records.
Do NOT use this method directly. Call create() method instead.
- Restore full name of plugin. If only platform name is specified
plugin with name existing@<platform_name> is going to be used
- Validates spec using standard plugin validation mechanism
- Creates env and platforms DB records in DB
:returns: dict that contains env record stored in DB
"""
for p_name, p_spec in spec.items():
if "@" not in p_name:
spec["existing@%s" % p_name] = p_spec
spec.pop(p_name)
errors = []
for p_name, p_spec in spec.items():
errors.extend(platform.Platform.validate(p_name, {}, spec, p_spec))
if errors:
raise exceptions.ManagerInvalidSpec(
mgr="Env", spec=spec, errors=errors)
_platforms = []
for p_name, p_spec in spec.items():
_platforms.append({
"status": platform.STATUS.INIT,
"plugin_name": p_name,
"plugin_spec": p_spec,
"platform_name": p_name.split("@")[1]
})
return cls(db.env_create(name, STATUS.INIT, description, extras,
spec, _platforms))
def _create_platforms(self):
"""Iterates over platform and creates them, storing results in DB.
Do NOT use this method directly! Use create() instead.
All platform statuses are going to be updated.
- If everything is OK all platforms and env would have READY statuts.
- If some of platforms failed, it will get status "FAILED TO CREATE"
as well as Env, all following platforms would have "SKIPPED" state.
- If there are issues with DB, and we can't store results to DB,
platform will be destroyed so we won't keep messy env, everything
will be logged.
This is not ideal solution, but it's best that we can do at the moment
"""
new_env_status = STATUS.READY
for p in self._get_platforms():
if new_env_status != STATUS.READY:
db.platform_set_status(
p.uuid, platform.STATUS.INIT, platform.STATUS.SKIPPED)
continue
try:
platform_data, plugin_data = p.create()
except Exception:
new_env_status = STATUS.FAILED_TO_CREATE
LOG.exception(
"Failed to create platform (%(uuid)s): "
"%(name)s with spec: %(spec)s" %
{"uuid": p.uuid, "name": p.get_fullname(), "spec": p.spec})
try:
db.platform_set_status(p.uuid, platform.STATUS.INIT,
platform.STATUS.FAILED_TO_CREATE)
except Exception:
LOG.Exception(
"Failed to set platform %(uuid)s status %(status)s"
% {"uuid": p.uuid,
"status": platform.STATUS.FAILED_TO_CREATE})
if new_env_status == STATUS.FAILED_TO_CREATE:
continue
try:
db.platform_set_data(
p.uuid,
platform_data=platform_data, plugin_data=plugin_data)
db.platform_set_status(
p.uuid, platform.STATUS.INIT, platform.STATUS.READY)
except Exception:
new_env_status = STATUS.FAILED_TO_CREATE
# NOTE(boris-42): We can't store platform data, because of
# issues with DB, to keep env clean we must
# destroy platform while we have complete data.
p.status = platform.STATUS.FAILED_TO_CREATE
p.platform_data, p.plugin_data = platform_data, plugin_data
try:
p.destroy()
LOG.warrning("Couldn't store platform %s data to DB."
"Attempt to destroy it succeeded." % p.uuid)
except Exception:
LOG.exception(
"Couldn't store data of platform(%(uuid)s): %(name)s "
"with spec: %(spec)s. Attempt to destroy it failed. "
"Sorry, but we can't do anything else for you. :("
% {"uuid": p.uuid,
"name": p.get_fullname(),
"spec": p.spec})
db.env_set_status(self.uuid, STATUS.INIT, new_env_status)
@classmethod
def create(cls, name, description, extras, spec):
"""Creates DB record for new env and returns instance of Env class.
:param name: User specified name of env
:param description: User specified description
:param extras: User specified dict with extra options
:param spec: Specification that contains info about all
platform plugins and their arguments.
:returns: EnvManager instance corresponding to created Env
"""
self = cls._validate_and_create_env(name, description, extras, spec)
self._create_platforms()
return self
def rename(self, new_name):
"""Renames env record.
:param new_name: New Env name.
"""
if self._env["name"] == new_name:
return True
return db.env_rename(self.uuid, self._env["name"], new_name)
def update(self, description=None, extras=None):
"""Update description and extras for environment.
:param description: New description for env
:param extras: New extras for env
"""
values = {}
if description and description != self._env["description"]:
values["description"] = description
if extras and extras != self._env["extras"]:
values["extras"] = extras
if values:
return db.env_update(self.uuid, **values)
return True
def update_spec(self, new_spec):
"""Update env spec. [not implemented]"""
# NOTE(boris-42): This functionality requires proper implementation of
# state machine and journal execution, which we are
# going to implement later for all code base
raise NotImplementedError()
_HEALTH_FORMAT = {
"type": "object",
"properties": {
"available": {"type": "boolean"},
"message": {"type": "string"},
"traceback": {"type": "array", "minItems": 3, "maxItems": 3}
},
"required": ["available"],
"additionalProperties": False
}
def check_health(self):
"""Iterates over all platforms in env and returns their health.
Format of result is
{
"platform_name": {
"available": True/False,
"message": "custom message"},
"traceback": ...
}
:return: Dict with results
"""
result = {}
for p in self._get_platforms():
try:
check_result = p.check_health()
jsonschema.validate(check_result, self._HEALTH_FORMAT)
check_result.setdefault("message", "OK!")
except Exception as e:
msg = ("Plugin %s.check_health() method is broken"
% p.get_fullname())
LOG.exception(msg)
check_result = {"message": msg, "available": False}
if not isinstance(e, jsonschema.ValidationError):
check_result["traceback"] = sys.exc_info()
result[p.get_fullname()] = check_result
return result
_INFO_FORMAT = {
"type": "object",
"properties": {
"info": {},
"error": {"type": "string"},
"traceback": {"type": "array", "minItems": 3, "maxItems": 3},
},
"required": ["info"],
"additionalProperties": False
}
def get_info(self):
"""Get detailed information about all platforms.
Platform plugins may collect any information from plugin and return
it back as a dict.
"""
result = {}
for p in self._get_platforms():
try:
info = p.info()
jsonschema.validate(info, self._INFO_FORMAT)
except Exception as e:
msg = "Plugin %s.info() method is broken" % p.get_fullname()
LOG.exception(msg)
info = {"info": None, "error": msg}
if not isinstance(e, jsonschema.ValidationError):
info["traceback"] = sys.exc_info()
result[p.get_fullname()] = info
return result
_CLEANUP_FORMAT = {
"type": "object",
"properties": {
"discovered": {"type": "integer"},
"deleted": {"type": "integer"},
"failed": {"type": "integer"},
"resources": {
"*": {
"type": "object",
"properties": {
"discovered": {"type": "integer"},
"deleted": {"type": "integer"},
"failed": {"type": "integer"}
},
"required": ["discovered", "deleted", "failed"],
"additionalProperties": False
}
},
"errors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"resource_id": {"type": "string"},
"resource_type": {"type": "string"},
"message": {"type": "string"},
"traceback": {
"type": "array",
"minItems": 3,
"maxItems": 3
}
},
"required": ["message"],
"additionalProperties": False
}
}
},
"required": ["discovered", "deleted", "failed", "resources", "errors"],
"additionalProperties": False
}
def cleanup(self, task_uuid=None):
"""Cleans all platform in env.
:param task_uuid: Cleans up only resources of specific task.
:returns: Dict with status of all cleanups
"""
db.env_set_status(self.uuid, STATUS.READY, STATUS.CLEANING)
result = {}
for p in self._get_platforms():
try:
cleanup_info = p.cleanup(task_uuid)
jsonschema.validate(cleanup_info, self._CLEANUP_FORMAT)
except Exception as e:
msg = "Plugin %s.cleanup() method is broken" % p.get_fullname()
LOG.exception(msg)
cleanup_info = {
"discovered": 0, "deleted": 0, "failed": 0,
"resources": {}, "errors": [{"message": msg}]
}
if not isinstance(e, jsonschema.ValidationError):
cleanup_info["errors"][0]["traceback"] = sys.exc_info()
result[p.get_fullname()] = cleanup_info
db.env_set_status(self.uuid, STATUS.CLEANING, STATUS.READY)
return result
def destroy(self, skip_cleanup=False):
"""Destroys all platforms related to env.
:param skip_cleanup: By default, before destroying plaform it's cleaned
"""
cleanup_info = {"skipped": True}
if not skip_cleanup:
cleanup_info = self.cleanup()
cleanup_info["skipped"] = False
if cleanup_info["errors"]:
return {
"cleanup_info": cleanup_info,
"destroy_info": {
"skipped": True,
"platforms": {},
"message": "Skipped because cleanup failed"
}
}
result = {
"cleanup_info": cleanup_info,
"destroy_info": {
"skipped": False,
"platforms": {}
}
}
db.env_set_status(self.uuid, STATUS.READY, STATUS.DESTROYING)
platforms = result["destroy_info"]["platforms"]
new_env_status = STATUS.DESTROYED
for p in self._get_platforms():
name = p.get_fullname()
platforms[name] = {"status": {"old": p.status}}
if p.status == platform.STATUS.DESTROYED:
platforms[name]["status"]["new"] = p.status
platforms[name]["message"] = (
"Platform is already destroyed. Do nothing")
continue
db.platform_set_status(
p.uuid, p.status, platform.STATUS.DESTROYING)
try:
p.destroy()
except Exception:
db.platform_set_status(p.uuid,
platform.STATUS.DESTROYING,
platform.STATUS.FAILED_TO_DESTROY)
platforms[name]["message"] = "Failed to destroy"
platforms[name]["status"]["new"] = (
platform.STATUS.FAILED_TO_DESTROY)
platforms[name]["traceback"] = sys.exc_info()
new_env_status = STATUS.FAILED_TO_DESTROY
else:
db.platform_set_status(p.uuid,
platform.STATUS.DESTROYING,
platform.STATUS.DESTROYED)
platforms[name]["message"] = "Successfully destroyed"
platforms[name]["status"]["new"] = platform.STATUS.DESTROYED
db.env_set_status(self.uuid, STATUS.DESTROYING, new_env_status)
return result
def delete(self, force=False):
"""Cascade delete of DB records related to env.
It deletes all Task and Verify results related to this env as well.
:param Force: Use it if you don't want to perform status check
"""
_status = self.status
if not force and _status != STATUS.DESTROYED:
raise exceptions.ManagerInvalidState(
mgr="Env", expected=STATUS.DESTROYED, actual=_status)
db.env_delete_cascade(self.uuid)

113
rally/env/platform.py vendored Normal file
View File

@ -0,0 +1,113 @@
# 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.
from rally.common.plugin import plugin
from rally.common import utils
from rally.common import validation
class _PlatformStatus(utils.ImmutableMixin, utils.EnumMixin):
"""Rally Env Statuses."""
INIT = "INITIALIZING"
SKIPPED = "SKIPPED"
READY = "READY"
FAILED_TO_CREATE = "FAILED TO CREATE"
DESTROYING = "DESTROYING"
FAILED_TO_DESTROY = "FAILED TO DESTROY"
DESTROYED = "DESTROYED"
TRANSITION_TABLE = {
INIT: (READY, SKIPPED, FAILED_TO_CREATE),
READY: (DESTROYING, ),
FAILED_TO_CREATE: (DESTROYING, ),
DESTROYING: (DESTROYED, FAILED_TO_DESTROY),
FAILED_TO_DESTROY: (DESTROYING, )
}
STATUS = _PlatformStatus()
def configure(name, platform):
"""Configure platform plugin.
Platform is building block for Env.
:param name: str platform plugin name
:param platform: str thing that is described by this plugin
"""
def wrapper(cls):
return plugin.configure(name=name, platform=platform)(cls)
return wrapper
@validation.add_default("jsonschema")
@plugin.base()
class Platform(plugin.Plugin, validation.ValidatablePluginMixin):
def __init__(self, spec,
uuid=None, plugin_data=None, platform_data=None, status=None):
"""Create instance of platform and validates config.
:param platform_config: Platform configuration file
:param platform_data: Platform specific data returned by create method
"""
self.spec = spec
self.uuid = uuid
self.plugin_data = plugin_data
self.platform_data = platform_data
self.status = status
def create(self):
"""Perform operations required to create platform.
:returns: Complete platform data as dictionary
"""
raise NotImplementedError(
"Platform %s doesn't support create action" % self.get_fullname())
def destroy(self):
"""Destroys platform."""
raise NotImplementedError(
"Platform %s doesn't support destroy action" % self.get_fullname())
def update(self, new_spec):
"""Updates existing platform config and returns new platform data.
:param new_platform_config: New platform config.
:returns: Complete platform data as dictionary
"""
raise NotImplementedError(
"Platform %s doesn't support update action" % self.get_fullname())
def cleanup(self, task_uuid=None):
"""Disaster cleanup for platform."""
raise NotImplementedError(
"Platform %s doesn't support cleanup action" % self.get_fullname())
def check_health(self):
"""Check whatever platform is alive."""
raise NotImplementedError(
"Platform %s doesn't support health check action"
% self.get_fullname())
def info(self):
"""Return information about platform as dictionary."""
raise NotImplementedError(
"Platform %s doesn't support info action" % self.get_fullname())

View File

@ -85,6 +85,27 @@ class DBRecordExists(DBException):
msg_fmt = "Record with %(field)s = %(value)s already exists in %(table)s"
class ManagerException(RallyException):
error_code = 500
msg_fmt = "Internal error: %(message)s"
class ManagerInvalidSpec(ManagerException):
error_code = 409
msg_fmt = "%(mgr)s manager got invalid spec: \n%(errors)s"
def __init__(self, **kwargs):
kwargs["errors"] = "\n".join(kwargs["errors"])
self.spec = kwargs.pop("spec", "")
super(ManagerInvalidSpec, self).__init__(**kwargs)
class ManagerInvalidState(ManagerException):
error_code = 500
msg_fmt = ("%(mgr)s manager in invalid state "
"expected `%(expected)s' actual `%(actual)s' ")
class InvalidArgumentsException(RallyException):
error_code = 455
msg_fmt = "Invalid arguments: '%(message)s'"

View File

@ -606,6 +606,220 @@ class WorkloadDataTestCase(test.DBTestCase):
self.assertEqual(self.workload_uuid, workload_data["workload_uuid"])
class EnvTestCase(test.DBTestCase):
def test_env_get(self):
env1 = db.env_create("name", "STATUS42", "descr", {}, {}, [])
env2 = db.env_create("name2", "STATUS42", "descr", {}, {}, [])
self.assertEqual(env1, db.env_get(env1["uuid"]))
self.assertEqual(env1, db.env_get(env1["name"]))
self.assertEqual(env2, db.env_get(env2["uuid"]))
self.assertEqual(env2, db.env_get(env2["name"]))
def test_env_get_not_found(self):
self.assertRaises(exceptions.DBRecordNotFound,
db.env_get, "non-existing-env")
def test_env_get_status(self):
env = db.env_create("name", "STATUS42", "descr", {}, {}, [])
self.assertEqual("STATUS42", db.env_get_status(env["uuid"]))
def test_env_get_status_non_existing(self):
self.assertRaises(exceptions.DBRecordNotFound,
db.env_get_status, "non-existing-env")
def test_env_list(self):
for i in range(3):
db.env_create("name %s" % i, "STATUS42", "descr", {}, {}, [])
self.assertEqual(i + 1, len(db.env_list()))
all_ = db.env_list()
self.assertIsInstance(all_, list)
for env in all_:
self.assertIsInstance(env, dict)
self.assertEqual(set("name %s" % i for i in range(3)),
set(e["name"] for e in db.env_list()))
def test_env_list_filter_by_status(self):
db.env_create("name 1", "STATUS42", "descr", {}, {}, [])
db.env_create("name 2", "STATUS42", "descr", {}, {}, [])
db.env_create("name 3", "STATUS43", "descr", {}, {}, [])
result = db.env_list("STATUS42")
self.assertEqual(2, len(result))
self.assertEqual(set(["name 1", "name 2"]),
set(r["name"] for r in result))
result = db.env_list("STATUS43")
self.assertEqual(1, len(result))
self.assertEqual("name 3", result[0]["name"])
def test_env_create(self):
env = db.env_create(
"name", "status", "descr", {"extra": "test"}, {"spec": "spec"}, [])
self.assertIsInstance(env, dict)
self.assertIsNotNone(env["uuid"])
self.assertEqual(env, db.env_get(env["uuid"]))
self.assertEqual("name", env["name"])
self.assertEqual("status", env["status"])
self.assertEqual("descr", env["description"])
self.assertEqual({"extra": "test"}, env["extras"])
self.assertEqual({"spec": "spec"}, env["spec"])
def test_env_create_duplicate_env(self):
db.env_create("name", "status", "descr", {}, {}, [])
self.assertRaises(exceptions.DBRecordExists,
db.env_create, "name", "status", "descr", {}, {}, [])
def teet_env_create_with_platforms(self):
platforms = [
{
"status": "ANY",
"plugin_name": "plugin_%s@plugin" % i,
"plugin_spec": {},
"platform_name": "plugin"
}
for i in range(3)
]
env = db.env_create("name", "status", "descr", {}, {}, platforms)
db_platforms = db.platforms_list(env["uuid"])
self.assertEqual(3, len(db_platforms))
def test_env_rename(self):
env = db.env_create(
"name", "status", "descr", {"extra": "test"}, {"spec": "spec"}, [])
self.assertTrue(db.env_rename(env["uuid"], env["name"], "name2"))
self.assertEqual("name2", db.env_get(env["uuid"])["name"])
def test_env_rename_duplicate(self):
env1 = db.env_create("name", "status", "descr", {}, {}, [])
env2 = db.env_create("name2", "status", "descr", {}, {}, [])
self.assertRaises(
exceptions.DBRecordExists,
db.env_rename, env1["uuid"], env1["name"], env2["name"])
def test_env_update(self):
env = db.env_create("name", "status", "descr", {}, {}, [])
self.assertTrue(db.env_update(env["uuid"]))
self.assertTrue(
db.env_update(env["uuid"], "another_descr", {"extra": 123}))
env = db.env_get(env["uuid"])
self.assertEqual("another_descr", env["description"])
self.assertEqual({"extra": 123}, env["extras"])
def test_evn_set_status(self):
env = db.env_create("name", "status", "descr", {}, {}, [])
self.assertRaises(
exceptions.DBConflict,
db.env_set_status, env["uuid"], "wrong_old_status", "new_status")
env = db.env_get(env["uuid"])
self.assertEqual("status", env["status"])
self.assertTrue(
db.env_set_status(env["uuid"], "status", "new_status"))
env = db.env_get(env["uuid"])
self.assertEqual("new_status", env["status"])
def test_env_delete_cascade(self):
platforms = [
{
"status": "ANY",
"plugin_name": "plugin_%s@plugin" % i,
"plugin_spec": {},
"platform_name": "plugin"
}
for i in range(3)
]
env = db.env_create("name", "status", "descr", {}, {}, platforms)
db.env_delete_cascade(env["uuid"])
self.assertEqual(0, len(db.env_list()))
self.assertEqual(0, len(db.platforms_list(env["uuid"])))
class PlatformTestCase(test.DBTestCase):
def setUp(self):
super(PlatformTestCase, self).setUp()
platforms = [
{
"status": "ANY",
"plugin_name": "plugin_%s@plugin" % i,
"plugin_spec": {},
"platform_name": "plugin"
}
for i in range(5)
]
self.env1 = db.env_create("env1", "init", "", {}, {}, platforms[:2])
self.env2 = db.env_create("env2", "init", "", {}, {}, platforms[2:])
def test_platform_get(self):
for p in db.platforms_list(self.env1["uuid"]):
self.assertEqual(p, db.platform_get(p["uuid"]))
def test_platform_get_not_found(self):
self.assertRaises(exceptions.DBRecordNotFound,
db.platform_get, "non_existing")
def test_platforms_list(self):
self.assertEqual(0, len(db.platforms_list("non_existing")))
self.assertEqual(2, len(db.platforms_list(self.env1["uuid"])))
self.assertEqual(3, len(db.platforms_list(self.env2["uuid"])))
def test_platform_set_status(self):
platforms = db.platforms_list(self.env1["uuid"])
self.assertRaises(
exceptions.DBConflict,
db.platform_set_status,
platforms[0]["uuid"], "OTHER", "NEW_STATUS")
self.assertEqual("ANY",
db.platform_get(platforms[0]["uuid"])["status"])
self.assertTrue(db.platform_set_status(
platforms[0]["uuid"], "ANY", "NEW_STATUS"))
self.assertEqual("NEW_STATUS",
db.platform_get(platforms[0]["uuid"])["status"])
self.assertEqual("ANY",
db.platform_get(platforms[1]["uuid"])["status"])
def test_platform_set_data(self):
platforms = db.platforms_list(self.env1["uuid"])
uuid = platforms[0]["uuid"]
self.assertTrue(db.platform_set_data(uuid))
self.assertTrue(
db.platform_set_data(uuid, platform_data={"platform": "data"}))
in_db = db.platform_get(uuid)
self.assertEqual({"platform": "data"}, in_db["platform_data"])
self.assertEqual({}, in_db["plugin_data"])
self.assertTrue(
db.platform_set_data(uuid, plugin_data={"plugin": "data"}))
in_db = db.platform_get(uuid)
self.assertEqual({"platform": "data"}, in_db["platform_data"])
self.assertEqual({"plugin": "data"}, in_db["plugin_data"])
self.assertTrue(
db.platform_set_data(uuid, platform_data={"platform": "data2"}))
in_db = db.platform_get(uuid)
self.assertEqual({"platform": "data2"}, in_db["platform_data"])
self.assertEqual({"plugin": "data"}, in_db["plugin_data"])
self.assertFalse(db.platform_set_data(
"non_existing", platform_data={}))
in_db = db.platform_get(uuid)
# just check that nothing changed after wrong uuid passed
self.assertEqual({"platform": "data2"}, in_db["platform_data"])
class DeploymentTestCase(test.DBTestCase):
def test_deployment_create(self):
deploy = db.deployment_create({"config": {"opt": "val"}})

View File

@ -39,8 +39,6 @@ class FakeSerializable(object):
@ddt.ddt
class SerializeTestCase(test.DBTestCase):
def setUp(self):
super(SerializeTestCase, self).setUp()
@ddt.data(
{"data": 1, "serialized": 1},
@ -80,3 +78,14 @@ class SerializeTestCase(test.DBTestCase):
return Fake()
self.assertRaises(exceptions.DBException, fake_method)
class ModelQueryTestCase(test.DBTestCase):
def test_model_query_wrong_model(self):
class Foo(object):
pass
self.assertRaises(exceptions.DBException,
db_api.Connection().model_query, Foo)

0
tests/unit/env/__init__.py vendored Normal file
View File

652
tests/unit/env/test_env_mgr.py vendored Normal file
View File

@ -0,0 +1,652 @@
# 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 mock
from rally.env import env_mgr
from rally.env import platform
from rally import exceptions
from tests.unit import test
class EnvManagerTestCase(test.TestCase):
def test_init(self):
data = {"uuid": "any", "balbalba": "balbal"}
mgr = env_mgr.EnvManager(data)
self.assertEqual("any", mgr.uuid)
self.assertEqual(data, mgr._env)
@mock.patch("rally.common.db.env_get_status")
def test_status_property(self, mock_env_get_status):
self.assertEqual(mock_env_get_status.return_value,
env_mgr.EnvManager({"uuid": "any"}).status)
mock_env_get_status.assert_called_once_with("any")
@mock.patch("rally.common.db.platforms_list")
@mock.patch("rally.common.db.env_get")
def test_data_property(self, mock_env_get, mock_platforms_list):
mock_platforms_list.return_value = [42, 43, 44]
mock_env_get.return_value = {
"id": "66",
"uuid": "666",
"created_at": mock.Mock(),
"updated_at": mock.Mock(),
"name": "42",
"description": "Some description",
"status": "some status",
"spec": "some_spec",
"extras": "some_extras",
}
result = env_mgr.EnvManager({"uuid": 111}).data
for field in ["name", "description", "status", "spec", "extras",
"created_at", "updated_at", "uuid", "id"]:
self.assertEqual(mock_env_get.return_value[field], result[field])
self.assertEqual(mock_platforms_list.return_value, result["platforms"])
mock_platforms_list.assert_called_once_with(111)
mock_env_get.assert_called_once_with(111)
@mock.patch("rally.common.db.platforms_list")
def test__get_platforms(self, mock_platforms_list):
@platform.configure(name="some", platform="foo")
class FooPlatform(platform.Platform):
pass
mock_platforms_list.side_effect = [
[],
[
{
"uuid": "1",
"plugin_name": "some@foo",
"plugin_data": "plugin_data",
"plugin_spec": "plugin_data",
"platform_data": "platform_data",
"status": "INIT"
},
{
"uuid": "2",
"plugin_name": "some@foo",
"plugin_data": None,
"plugin_spec": "plugin_data",
"platform_data": None,
"status": "CREATED"
},
]
]
self.assertEqual([], env_mgr.EnvManager({"uuid": 42})._get_platforms())
mock_platforms_list.assert_called_once_with(42)
result = env_mgr.EnvManager({"uuid": 43})._get_platforms()
self.assertEqual(2, len(result))
for i, r in enumerate(sorted(result, key=lambda x: x.uuid)):
self.assertIsInstance(r, FooPlatform)
self.assertEqual("some@foo", r.get_fullname())
self.assertEqual(str(i + 1), r.uuid)
mock_platforms_list.assert_has_calls([mock.call(42), mock.call(43)])
@mock.patch("rally.common.db.env_get",
return_value={"uuid": "1"})
def test_get(self, mock_env_get):
self.assertEqual("1", env_mgr.EnvManager.get("1").uuid)
mock_env_get.assert_called_once_with("1")
@mock.patch("rally.common.db.env_list",
return_value=[{"uuid": "1"}, {"uuid": "2"}])
def test_list(self, mock_env_list):
result = env_mgr.EnvManager.list()
for r in result:
self.assertIsInstance(r, env_mgr.EnvManager)
self.assertEqual(set(r.uuid for r in result), set(["1", "2"]))
self.assertEqual(2, len(result))
mock_env_list.assert_called_once_with(status=None)
@mock.patch("rally.common.db.env_create")
def test__validate_and_create_env_empty_spec(self, mock_env_create):
mock_env_create.return_value = {"uuid": "1"}
env = env_mgr.EnvManager._validate_and_create_env(
"name", "descr", {"extras": ""}, {})
self.assertIsInstance(env, env_mgr.EnvManager)
self.assertEqual("1", env.uuid)
mock_env_create.assert_called_once_with(
"name", env_mgr.STATUS.INIT, "descr", {"extras": ""}, {}, [])
@mock.patch("rally.common.db.env_create")
def test__validate_and_create_env_with_spec(self, mock_env_create):
mock_env_create.return_value = {"uuid": "1"}
@platform.configure("existing", platform="valid1")
class Platform1(platform.Platform):
CONFIG_SCHEMA = {
"type": "object",
"properties": {"a": {"type": "string"}},
"additionalProperties": False
}
@platform.configure("2", platform="valid2")
class Platform2(platform.Platform):
CONFIG_SCHEMA = {
"type": "object",
"properties": {"b": {"type": "string"}},
"additionalProperties": False
}
self.addCleanup(Platform1.unregister)
self.addCleanup(Platform2.unregister)
env_mgr.EnvManager._validate_and_create_env(
"n", "d", "ext", {"valid1": {"a": "str"}})
expected_platforms = [{
"status": platform.STATUS.INIT,
"plugin_name": "existing@valid1",
"plugin_spec": {"a": "str"},
"platform_name": "valid1"
}]
mock_env_create.assert_called_once_with(
"n", env_mgr.STATUS.INIT, "d", "ext",
{"existing@valid1": {"a": "str"}}, expected_platforms)
mock_env_create.reset_mock()
self.assertRaises(
exceptions.ManagerInvalidSpec,
env_mgr.EnvManager._validate_and_create_env,
"n", "d", "ext", {"valid1": {"a": "str"}, "2@valid2": {"c": 1}}
)
self.assertFalse(mock_env_create.called)
mock_env_create.reset_mock()
self.assertRaises(
exceptions.RallyException,
env_mgr.EnvManager._validate_and_create_env,
"n", "d", "ext", {"non_existing@nope": {"a": "str"}}
)
self.assertFalse(mock_env_create.called)
@mock.patch("rally.common.db.env_set_status")
@mock.patch("rally.common.db.platform_set_data")
@mock.patch("rally.common.db.platform_set_status")
@mock.patch("rally.common.db.platforms_list")
def test__create_platforms(self,
mock_platforms_list, mock_platform_set_status,
mock_platform_set_data, mock_env_set_status):
# One platform that passes successfully
@platform.configure("passes", platform="create")
class ValidPlatform(platform.Platform):
def create(self):
return {"platform": "data"}, {"plugin": "data"}
self.addCleanup(ValidPlatform.unregister)
mock_platforms_list.return_value = [
{
"uuid": "p_uuid",
"plugin_name": "passes@create",
"plugin_spec": {},
"plugin_data": None,
"platform_data": None,
"status": platform.STATUS.INIT
}
]
env_mgr.EnvManager({"uuid": 121})._create_platforms()
mock_platforms_list.assert_called_once_with(121)
mock_platform_set_status.assert_called_once_with(
"p_uuid", platform.STATUS.INIT, platform.STATUS.READY)
mock_platform_set_data.assert_called_once_with(
"p_uuid",
platform_data={"platform": "data"}, plugin_data={"plugin": "data"})
mock_env_set_status.assert_called_once_with(
121, env_mgr.STATUS.INIT, env_mgr.STATUS.READY)
@mock.patch("rally.common.db.env_set_status")
@mock.patch("rally.common.db.platform_set_status")
@mock.patch("rally.env.env_mgr.EnvManager._get_platforms")
def test__create_platforms_failed(self, mock__get_platforms,
mock_platform_set_status,
mock_env_set_status):
# Check when first fails, second is marked as skipped
@platform.configure("bad", platform="create")
class InValidPlatform(platform.Platform):
def create(self):
raise Exception("I don't want to work!")
@platform.configure("good_but_skipped", platform="create")
class ValidPlatform(platform.Platform):
def create(self):
return {"platform": "data"}, {"plugin": "data"}
for p in [InValidPlatform, ValidPlatform]:
self.addCleanup(p.unregister)
mock__get_platforms.return_value = [
InValidPlatform({}, uuid=1), ValidPlatform({}, uuid=2)]
env_mgr.EnvManager({"uuid": 42})._create_platforms()
mock_env_set_status.assert_called_once_with(
42, env_mgr.STATUS.INIT, env_mgr.STATUS.FAILED_TO_CREATE)
mock_platform_set_status.assert_has_calls([
mock.call(1, platform.STATUS.INIT,
platform.STATUS.FAILED_TO_CREATE),
mock.call(2, platform.STATUS.INIT, platform.STATUS.SKIPPED),
])
@mock.patch("rally.common.db.env_set_status")
@mock.patch("rally.common.db.platform_set_status")
@mock.patch("rally.common.db.platform_set_data")
@mock.patch("rally.env.env_mgr.EnvManager._get_platforms")
def test__create_platforms_when_db_issues_autodestroy(
self, mock__get_platforms, mock_platform_set_data,
mock_platform_set_status, mock_env_set_status):
# inject db errors check that auto destroy is called
platform1 = mock.MagicMock()
platform1.uuid = 11
platform1.destroy.side_effect = Exception
platform1.create.return_value = ("platform_d", "plugin_d")
platform2 = mock.MagicMock()
platform2.uuid = 22
mock__get_platforms.return_value = [platform1, platform2]
mock_platform_set_data.side_effect = Exception
env_mgr.EnvManager({"uuid": 42})._create_platforms()
mock_platform_set_status.assert_called_once_with(
22, platform.STATUS.INIT, platform.STATUS.SKIPPED)
mock_platform_set_data.assert_called_once_with(
11, platform_data="platform_d", plugin_data="plugin_d")
mock_env_set_status.assert_called_once_with(
42, env_mgr.STATUS.INIT, env_mgr.STATUS.FAILED_TO_CREATE)
@mock.patch("rally.common.db.platforms_list", return_value=[])
@mock.patch("rally.common.db.env_set_status")
@mock.patch("rally.common.db.env_create", return_value={"uuid": 121})
def test_create(self, mock_env_create, mock_env_set_status,
mock_platforms_list):
# NOTE(boris-42): Just check with empty spec that just check workflow
result = env_mgr.EnvManager.create("a", "descr", "extras", {})
self.assertIsInstance(result, env_mgr.EnvManager)
self.assertEqual(121, result.uuid)
@mock.patch("rally.common.db.env_rename")
def test_rename(self, mock_env_rename):
env = env_mgr.EnvManager({"uuid": "11", "name": "n"})
self.assertTrue(env.rename, env.rename("n"))
self.assertEqual(0, mock_env_rename.call_count)
self.assertTrue(env.rename, env.rename("n2"))
mock_env_rename.assert_called_once_with("11", "n", "n2")
@mock.patch("rally.common.db.env_update")
def test_update(self, mock_env_update):
env = env_mgr.EnvManager(
{"uuid": "11", "description": "d", "extras": "e"})
self.assertTrue(env.update())
self.assertEqual(0, mock_env_update.call_count)
env.update(description="d2", extras="e2")
mock_env_update.assert_called_once_with(
"11", description="d2", extras="e2")
def test_update_spec(self):
self.assertRaises(NotImplementedError,
env_mgr.EnvManager({"uuid": 1}).update_spec, "")
@mock.patch("rally.env.env_mgr.EnvManager._get_platforms")
def test_check_health(self, mock__get_platforms):
valid_result = {
"available": False,
"message": "Nope I don't want to work"
}
@platform.configure(name="valid", platform="check")
class ValidPlatform(platform.Platform):
def check_health(self):
return valid_result
@platform.configure(name="broken_fromat", platform="check")
class BrokenFormatPlatform(platform.Platform):
def check_health(self):
return {"something": "is wrong here in format"}
@platform.configure(name="just_broken", platform="check")
class JustBrokenPlatform(platform.Platform):
def check_health(self):
raise Exception("This is really bad exception")
for p in [ValidPlatform, BrokenFormatPlatform, JustBrokenPlatform]:
self.addCleanup(p.unregister)
mock__get_platforms.side_effect = [
[ValidPlatform("spec1")],
[ValidPlatform("spec1"), BrokenFormatPlatform("spec2")],
[JustBrokenPlatform("spec3")]
]
self.assertEqual({"valid@check": valid_result},
env_mgr.EnvManager({"uuid": "42"}).check_health())
broken_msg = "Plugin %s.check_health() method is broken"
self.assertEqual(
{
"valid@check": valid_result,
"broken_fromat@check": {
"message": broken_msg % "broken_fromat@check",
"available": False
}
},
env_mgr.EnvManager({"uuid": "43"}).check_health())
self.assertEqual(
{
"just_broken@check": {
"message": broken_msg % "just_broken@check",
"available": False,
"traceback": (Exception, mock.ANY, mock.ANY)
}
},
env_mgr.EnvManager({"uuid": "44"}).check_health())
mock__get_platforms.assert_has_calls([mock.call()] * 3)
@mock.patch("rally.env.env_mgr.EnvManager._get_platforms")
def test_get_info(self, mock__get_platforms):
@platform.configure(name="valid", platform="info")
class InfoValid(platform.Platform):
def info(self):
return {"info": "it works!", "error": ""}
@platform.configure(name="wrong_fmt", platform="info")
class InfoWrongFormat(platform.Platform):
def info(self):
return {"something": "is wrong"}
@platform.configure(name="broken", platform="info")
class InfoBroken(platform.Platform):
def info(self):
raise Exception("This should not happen")
for p in [InfoValid, InfoWrongFormat, InfoBroken]:
self.addCleanup(p.unregister)
mock__get_platforms.side_effect = [
[InfoValid("spec1")],
[InfoValid("spec1"), InfoWrongFormat("spec2")],
[InfoValid("spec1"), InfoBroken("spec3")],
]
valid_result = {"info": "it works!", "error": ""}
self.assertEqual({"valid@info": valid_result},
env_mgr.EnvManager({"uuid": "42"}).get_info())
self.assertEqual(
{
"valid@info": valid_result,
"wrong_fmt@info": {
"error": "Plugin wrong_fmt@info.info() method is broken",
"info": None
}
},
env_mgr.EnvManager({"uuid": "43"}).get_info())
self.assertEqual(
{
"valid@info": valid_result,
"broken@info": {
"error": "Plugin broken@info.info() method is broken",
"info": None,
"traceback": (Exception, mock.ANY, mock.ANY)
}
},
env_mgr.EnvManager({"uuid": "44"}).get_info())
mock__get_platforms.assert_has_calls([mock.call()] * 3)
@mock.patch("rally.common.db.env_set_status")
@mock.patch("rally.env.env_mgr.EnvManager._get_platforms")
def test_cleanup(self, mock__get_platforms, mock_env_set_status):
valid_result = {
"discovered": 10,
"deleted": 6,
"failed": 4,
"resources": {
"vm": {
"discovered": 2,
"failed": 2,
"deleted": 0
}
},
"errors": [
{
"resource_id": "1",
"resource_type": "vm",
"message": "something"
}
]
}
@platform.configure(name="valid", platform="clean")
class CleanValid(platform.Platform):
def cleanup(self, task_uuid=None):
return valid_result
@platform.configure(name="wrong", platform="clean")
class CleanWrongFormat(platform.Platform):
def cleanup(self, task_uuid):
return {"something": "is wrong"}
@platform.configure(name="broken", platform="clean")
class CleanBroken(platform.Platform):
def cleanup(self, task_uuid):
raise Exception("This should not happen")
for p in [CleanValid, CleanWrongFormat, CleanBroken]:
self.addCleanup(p.unregister)
mock__get_platforms.return_value = [
CleanValid("spec1"),
CleanBroken("spec2"),
CleanWrongFormat("spec3")
]
result = env_mgr.EnvManager({"uuid": 424}).cleanup()
mock__get_platforms.assert_called_once_with()
mock_env_set_status.assert_has_calls([
mock.call(424, env_mgr.STATUS.READY, env_mgr.STATUS.CLEANING),
mock.call(424, env_mgr.STATUS.CLEANING, env_mgr.STATUS.READY)
])
self.assertIsInstance(result, dict)
self.assertEqual(3, len(result))
self.assertEqual(valid_result, result["valid@clean"])
self.assertEqual(
{
"discovered": 0, "deleted": 0, "failed": 0, "resources": {},
"errors": [{
"message": "Plugin wrong@clean.cleanup() method is broken",
}]
},
result["wrong@clean"]
)
self.assertEqual(
{
"discovered": 0, "deleted": 0, "failed": 0, "resources": {},
"errors": [{
"message": "Plugin broken@clean.cleanup() method is "
"broken",
"traceback": (Exception, mock.ANY, mock.ANY)
}]
},
result["broken@clean"]
)
@mock.patch("rally.env.env_mgr.EnvManager.cleanup",
return_value={"errors": [121]})
def test_destory_cleanup_failed(self, mock_env_manager_cleanup):
self.assertEqual(
{
"cleanup_info": {
"errors": [121],
"skipped": False
},
"destroy_info": {
"skipped": True,
"platforms": {},
"message": "Skipped because cleanup failed"
}
},
env_mgr.EnvManager({"uuid": 42}).destroy()
)
mock_env_manager_cleanup.assert_called_once_with()
@mock.patch("rally.env.env_mgr.EnvManager._get_platforms", return_value=[])
@mock.patch("rally.common.db.env_set_status")
def test_destroy_no_platforms(self,
mock_env_set_status, mock__get_platforms):
self.assertEqual(
{
"cleanup_info": {"skipped": True},
"destroy_info": {"skipped": False, "platforms": {}}
},
env_mgr.EnvManager({"uuid": 42}).destroy(skip_cleanup=True)
)
mock_env_set_status.assert_has_calls([
mock.call(42, env_mgr.STATUS.READY, env_mgr.STATUS.DESTROYING),
mock.call(42, env_mgr.STATUS.DESTROYING, env_mgr.STATUS.DESTROYED)
])
mock__get_platforms.assert_called_once_with()
@mock.patch("rally.common.db.env_set_status")
@mock.patch("rally.common.db.platform_set_status")
@mock.patch("rally.env.env_mgr.EnvManager._get_platforms")
def test_destory_with_platforms(self, mock__get_platforms,
mock_platform_set_status,
mock_env_set_status):
platform1 = mock.MagicMock()
platform1.get_fullname.return_value = "p_destroyed"
platform1.status = platform.STATUS.DESTROYED
platform2 = mock.MagicMock()
platform2.get_fullname.return_value = "p_valid"
platform2.status = platform.STATUS.READY
platform3 = mock.MagicMock()
platform3.get_fullname.return_value = "p_invalid"
platform3.destroy.side_effect = Exception
platform3.status = platform.STATUS.READY
mock__get_platforms.return_value = [
platform1, platform2, platform3
]
result = env_mgr.EnvManager({"uuid": 666}).destroy(skip_cleanup=True)
self.assertIsInstance(result, dict)
self.assertEqual(2, len(result))
self.assertEqual({"skipped": True}, result["cleanup_info"])
self.assertEqual(
{
"skipped": False,
"platforms": {
"p_destroyed": {
"message": "Platform is already destroyed. Do nothing",
"status": {
"old": platform.STATUS.DESTROYED,
"new": platform.STATUS.DESTROYED
},
},
"p_valid": {
"message": "Successfully destroyed",
"status": {
"old": platform.STATUS.READY,
"new": platform.STATUS.DESTROYED
},
},
"p_invalid": {
"message": "Failed to destroy",
"status": {
"old": platform.STATUS.READY,
"new": platform.STATUS.FAILED_TO_DESTROY
},
"traceback": (Exception, mock.ANY, mock.ANY)
}
}
},
result["destroy_info"])
mock__get_platforms.assert_called_once_with()
mock_platform_set_status.assert_has_calls([
mock.call(platform2.uuid,
platform.STATUS.READY,
platform.STATUS.DESTROYING),
mock.call(platform2.uuid,
platform.STATUS.DESTROYING,
platform.STATUS.DESTROYED),
mock.call(platform3.uuid,
platform.STATUS.READY,
platform.STATUS.DESTROYING),
mock.call(platform3.uuid,
platform.STATUS.DESTROYING,
platform.STATUS.FAILED_TO_DESTROY)
])
@mock.patch("rally.common.db.env_get_status")
@mock.patch("rally.common.db.env_delete_cascade")
def test_delete(self, mock_env_delete_cascade, mock_env_get_status):
mock_env_get_status.side_effect = [
"WRONG", env_mgr.STATUS.DESTROYED
]
self.assertRaises(exceptions.ManagerInvalidState,
env_mgr.EnvManager({"uuid": "42"}).delete)
self.assertFalse(mock_env_delete_cascade.called)
env_mgr.EnvManager({"uuid": "43"}).delete()
mock_env_delete_cascade.assert_called_once_with("43")
mock_env_get_status.assert_has_calls(
[mock.call("42"), mock.call("43")])
@mock.patch("rally.common.db.env_get_status")
@mock.patch("rally.common.db.env_delete_cascade")
def test_delete_force(self, mock_env_delete_cascade, mock_env_get_status):
mock_env_get_status.return_value = "WRONG"
env_mgr.EnvManager({"uuid": "44"}).delete(force=True)
mock_env_delete_cascade.assert_called_once_with("44")

44
tests/unit/env/test_platform.py vendored Normal file
View File

@ -0,0 +1,44 @@
# 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.
from rally.env import platform
from tests.unit import test
class PlatformTestCase(test.TestCase):
def test_plugin_configure_and_methods(self):
@platform.configure(name="existing", platform="foo")
class FooPlugin(platform.Platform):
pass
self.addCleanup(FooPlugin.unregister)
f = FooPlugin("spec", "uuid", "plugin_data", "platform_data", "status")
self.assertEqual(f.uuid, "uuid")
self.assertEqual(f.spec, "spec")
self.assertEqual(f.plugin_data, "plugin_data")
self.assertEqual(f.platform_data, "platform_data")
self.assertEqual(f.status, "status")
self.assertRaises(NotImplementedError, f.create)
self.assertRaises(NotImplementedError, f.destroy)
self.assertRaises(NotImplementedError, f.update, "new_spec")
self.assertRaises(NotImplementedError, f.cleanup)
self.assertRaises(NotImplementedError,
f.cleanup, task_uuid="task_uuid")
self.assertRaises(NotImplementedError, f.check_health)
self.assertRaises(NotImplementedError, f.info)