Move plugin from ostf_plugin repo

This commit is contained in:
Tatyanka 2013-09-17 18:37:55 +03:00
parent 080f5f1564
commit afbe1ea46c
70 changed files with 3861 additions and 7 deletions

10
.gitignore vendored
View File

@ -1,5 +1,11 @@
.idea
*.egg-info
dist
*.pyc
*.log
nosetests.xml
*.egg-info
/*.egg
.tox
build
dist
*.out
.coverage

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
recursive-include ostf_adapter *.ini
recursive-include ostf_adapter *

0
fuel_plugin/__init__.py Normal file
View File

View File

@ -0,0 +1,13 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,67 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2013 Mirantis, Inc.
#
# 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 logging
import signal
from fuel_plugin.ostf_adapter import cli_config
from fuel_plugin.ostf_adapter import nailgun_hooks
from fuel_plugin.ostf_adapter import logger
from fuel_plugin.ostf_adapter.nose_plugin import nose_discovery
from gevent import pywsgi
from fuel_plugin.ostf_adapter.wsgi import app
import pecan
def main():
cli_args = cli_config.parse_cli()
config = {
'server': {
'host': cli_args.host,
'port': cli_args.port
},
'dbpath': cli_args.dbpath,
'debug': cli_args.debug,
'debug_tests': cli_args.debug_tests
}
logger.setup(log_file=cli_args.log_file)
log = logging.getLogger(__name__)
root = app.setup_app(config=config)
nose_discovery.discovery(cli_args.debug_tests)
if getattr(cli_args, 'after_init_hook'):
return nailgun_hooks.after_initialization_environment_hook()
host, port = pecan.conf.server.host, pecan.conf.server.port
srv = pywsgi.WSGIServer((host, int(port)), root)
log.info('Starting server in PID %s', os.getpid())
log.info("serving on http://%s:%s", host, port)
try:
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
srv.serve_forever()
except KeyboardInterrupt:
pass
if __name__ == '__main__':
main()

View File

@ -0,0 +1,13 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,34 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 argparse
import sys
def parse_cli():
parser = argparse.ArgumentParser()
parser.add_argument('--after-initialization-environment-hook',
action='store_true', dest='after_init_hook')
parser.add_argument('--debug',
action='store_true', dest='debug')
parser.add_argument(
'--dbpath', metavar='DB_PATH',
default='postgresql+psycopg2://adapter:demo@localhost/testing_adapter')
parser.add_argument('--host', default='127.0.0.1')
parser.add_argument('--port', default='8989')
parser.add_argument('--log_file', default=None, metavar='PATH')
parser.add_argument('--nailgun-host', default='127.0.0.1')
parser.add_argument('--nailgun-port', default='3232')
parser.add_argument('--debug_tests', default=None)
return parser.parse_args(sys.argv[1:])

View File

@ -0,0 +1,38 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 logging
import logging.handlers
def setup(log_file=None):
formatter = logging.Formatter(
'[%(asctime)s] %(levelname)-8s %(message)s')
log = logging.getLogger(None)
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)
stream_handler.setFormatter(formatter)
log.addHandler(stream_handler)
if log_file:
log_file = os.path.abspath(log_file)
file_handler = logging.handlers.WatchedFileHandler(log_file)
file_handler.setLevel(logging.DEBUG)
mode = int('0644', 8)
os.chmod(log_file, mode)
file_handler.setFormatter(formatter)
log.addHandler(file_handler)
log.setLevel(logging.INFO)

View File

@ -0,0 +1,29 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 logging
from fuel_plugin.ostf_adapter.nose_plugin import nose_discovery
from fuel_plugin.ostf_adapter.storage import alembic_cli
from pecan import conf
LOG = logging.getLogger(__name__)
def after_initialization_environment_hook():
"""Expect 0 on success by nailgun
Exception is good enough signal that something goes wrong
"""
alembic_cli.do_apply_migrations()
nose_discovery.discovery(conf.debug_tests)
return 0

View File

@ -0,0 +1,31 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 stevedore import extension
_PLUGIN_MANAGER = None
def get_plugin(plugin):
global _PLUGIN_MANAGER
plugin_manager = _PLUGIN_MANAGER
if plugin_manager is None:
PLUGINS_NAMESPACE = 'plugins'
plugin_manager = extension.ExtensionManager(PLUGINS_NAMESPACE,
invoke_on_load=True)
_PLUGIN_MANAGER = plugin_manager
return _PLUGIN_MANAGER[plugin].obj

View File

@ -0,0 +1,105 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 logging
from pecan import conf
from fuel_plugin.ostf_adapter.nose_plugin import nose_storage_plugin
from fuel_plugin.ostf_adapter.nose_plugin import nose_test_runner
from fuel_plugin.ostf_adapter.nose_plugin import nose_utils
from fuel_plugin.ostf_adapter.storage import storage_utils, engine, models
LOG = logging.getLogger(__name__)
class NoseDriver(object):
def __init__(self):
LOG.warning('WTF')
self._named_threads = {}
session = engine.get_session()
with session.begin(subtransactions=True):
storage_utils.update_all_running_test_runs(session)
def check_current_running(self, unique_id):
return unique_id in self._named_threads
def run(self, test_run, test_set, tests=None):
"""
remove unneceserry arguments
spawn processes and send them tasks as to workers
"""
tests = tests or test_run.enabled_tests
if tests:
argv_add = [nose_utils.modify_test_name_for_nose(test) for test in
tests]
else:
argv_add = [test_set.test_path] + test_set.additional_arguments
self._named_threads[test_run.id] = nose_utils.run_proc(
self._run_tests, test_run.id, test_run.cluster_id, argv_add)
def _run_tests(self, test_run_id, cluster_id, argv_add):
session = engine.get_session()
try:
nose_test_runner.SilentTestProgram(
addplugins=[nose_storage_plugin.StoragePlugin(
test_run_id, str(cluster_id))],
exit=False,
argv=['ostf_tests'] + argv_add)
self._named_threads.pop(int(test_run_id), None)
except Exception, e:
LOG.exception('Test run: %s\n', test_run_id)
finally:
models.TestRun.update_test_run(
session, test_run_id, status='finished')
def kill(self, test_run_id, cluster_id, cleanup=None):
session = engine.get_session()
if test_run_id in self._named_threads:
self._named_threads[test_run_id].terminate()
self._named_threads.pop(test_run_id, None)
if cleanup:
nose_utils.run_proc(
self._clean_up,
test_run_id,
cluster_id,
cleanup)
else:
models.TestRun.update_test_run(
session, test_run_id, status='finished')
return True
return False
def _clean_up(self, test_run_id, external_id, cleanup):
session = engine.get_session()
try:
module_obj = __import__(cleanup, -1)
os.environ['NAILGUN_HOST'] = str(conf.nailgun.host)
os.environ['NAILGUN_PORT'] = str(conf.nailgun.port)
os.environ['CLUSTER_ID'] = str(external_id)
module_obj.cleanup.cleanup()
except Exception:
LOG.exception('EXCEPTION IN CLEANUP')
finally:
models.TestRun.update_test_run(
session, test_run_id, status='finished')

View File

@ -0,0 +1,89 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 logging
import os
from nose import plugins
from fuel_plugin.ostf_adapter.nose_plugin import nose_test_runner
from fuel_plugin.ostf_adapter.nose_plugin import nose_utils
from fuel_plugin.ostf_adapter.storage import engine, models
CORE_PATH = 'fuel_health'
LOG = logging.getLogger(__name__)
class DiscoveryPlugin(plugins.Plugin):
enabled = True
name = 'discovery'
score = 15000
def __init__(self):
self.test_sets = {}
super(DiscoveryPlugin, self).__init__()
def options(self, parser, env=os.environ):
pass
def configure(self, options, conf):
pass
def afterImport(self, filename, module):
module = __import__(module, fromlist=[module])
LOG.info('Inspecting %s', filename)
if hasattr(module, '__profile__'):
session = engine.get_session()
with session.begin(subtransactions=True):
LOG.info('%s discovered.', module.__name__)
test_set = models.TestSet(**module.__profile__)
test_set = session.merge(test_set)
session.add(test_set)
self.test_sets[test_set.id] = test_set
def addSuccess(self, test):
test_id = test.id()
for test_set_id in self.test_sets.keys():
if test_set_id in test_id:
session = engine.get_session()
with session.begin(subtransactions=True):
LOG.info('%s added for %s', test_id, test_set_id)
data = dict()
data['title'], data['description'], data['duration'] = \
nose_utils.get_description(test)
old_test_obj = session.query(models.Test).filter_by(
name=test_id, test_set_id=test_set_id,
test_run_id=None).\
update(data, synchronize_session=False)
if not old_test_obj:
data.update({'test_set_id': test_set_id,
'name': test_id})
test_obj = models.Test(**data)
session.add(test_obj)
def discovery(path=None):
"""
function to automaticly discover any test packages
"""
tests = [CORE_PATH, path] if path else [CORE_PATH]
LOG.info('Starting discovery for %r.', tests)
nose_test_runner.SilentTestProgram(
addplugins=[DiscoveryPlugin()],
exit=False,
argv=['tests_discovery', '--collect-only'] + tests)

View File

@ -0,0 +1,111 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 time import time
import logging
import os
from nose import plugins
from nose.suite import ContextSuite
from pecan import conf
from fuel_plugin.ostf_adapter.nose_plugin import nose_utils
from fuel_plugin.ostf_adapter.storage import models, engine
LOG = logging.getLogger(__name__)
class StoragePlugin(plugins.Plugin):
enabled = True
name = 'storage'
score = 15000
def __init__(
self, test_run_id, cluster_id):
self.test_run_id = test_run_id
self.cluster_id = cluster_id
super(StoragePlugin, self).__init__()
self._start_time = None
def options(self, parser, env=os.environ):
env['NAILGUN_HOST'] = str(conf.nailgun.host)
env['NAILGUN_PORT'] = str(conf.nailgun.port)
if self.cluster_id:
env['CLUSTER_ID'] = str(self.cluster_id)
def configure(self, options, conf):
self.conf = conf
def _add_message(
self, test, err=None, status=None):
data = {
'status': status,
'time_taken': self.taken
}
data['title'], data['description'], data['duration'] = \
nose_utils.get_description(test)
if err:
exc_type, exc_value, exc_traceback = err
data['step'], data['message'] = None, u''
if not status == 'error':
data['step'], data['message'] = \
nose_utils.format_failure_message(exc_value)
data['traceback'] = u''
else:
data['step'], data['message'] = None, u''
data['traceback'] = u''
session = engine.get_session()
with session.begin(subtransactions=True):
if isinstance(test, ContextSuite):
for sub_test in test._tests:
data['title'], data['description'], data['duration'] = \
nose_utils.get_description(test)
models.Test.add_result(
session, self.test_run_id, sub_test.id(), data)
else:
models.Test.add_result(
session, self.test_run_id, test.id(), data)
def addSuccess(self, test, capt=None):
self._add_message(test, status='success')
def addFailure(self, test, err):
LOG.error('%s', test.id(), exc_info=err)
self._add_message(test, err=err, status='failure')
def addError(self, test, err):
if err[0] == AssertionError:
LOG.error('%s', test.id(), exc_info=err)
self._add_message(
test, err=err, status='failure')
else:
LOG.error('%s', test.id(), exc_info=err)
self._add_message(test, err=err, status='error')
def beforeTest(self, test):
self._start_time = time()
self._add_message(test, status='running')
def describeTest(self, test):
return test.test._testMethodDoc
@property
def taken(self):
if self._start_time:
return time() - self._start_time
return 0

View File

@ -0,0 +1,36 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 nose import core
class SilentTestRunner(core.TextTestRunner):
def run(self, test):
"""Overrides to provide plugin hooks and defer all output to
the test result class.
"""
result = self._makeResult()
test(result)
return result
class SilentTestProgram(core.TestProgram):
def runTests(self):
"""Run Tests. Returns true on success, false on failure, and sets
self.success to the same value.
"""
self.testRunner = SilentTestRunner(stream=self.config.stream,
verbosity=0,
config=self.config)
return self.testRunner.run(self.test).wasSuccessful()

View File

@ -0,0 +1,102 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 traceback
import re
import json
import os
import multiprocessing
from nose import case
def parse_json_file(file_path):
current_directory = os.path.dirname(os.path.realpath(__file__))
commands_path = os.path.join(
current_directory, file_path)
with open(commands_path, 'r') as f:
return json.load(f)
def get_exc_message(exception_value):
"""
@exception_value - Exception type object
"""
_exc_long = str(exception_value)
if isinstance(_exc_long, basestring):
return _exc_long.split('\n')[0]
return u""
def get_description(test_obj):
if isinstance(test_obj, case.Test):
docstring = test_obj.shortDescription()
if docstring:
duration_pattern = r'Duration:.?(?P<duration>.+)'
duration_matcher = re.search(duration_pattern, docstring)
if duration_matcher:
duration = duration_matcher.group(1)
docstring = docstring[:duration_matcher.start()]
else:
duration = None
docstring = docstring.split('\n')
name = docstring.pop(0)
description = u'\n'.join(docstring) if docstring else u""
return name, description, duration
return u"", u"", u""
def modify_test_name_for_nose(test_path):
test_module, test_class, test_method = test_path.rsplit('.', 2)
return '{0}:{1}.{2}'.format(test_module, test_class, test_method)
def format_exception(exc_info):
ec, ev, tb = exc_info
# formatError() may have turned our exception object into a string, and
# Python 3's traceback.format_exception() doesn't take kindly to that (it
# expects an actual exception object). So we work around it, by doing the
# work ourselves if ev is a string.
if isinstance(ev, basestring):
tb_data = ''.join(traceback.format_tb(tb))
return tb_data + ev
else:
return ''.join(traceback.format_exception(*exc_info))
def format_failure_message(message):
message = get_exc_message(message)
matcher = re.search(
r'^[a-zA-Z]+\s?(\d+)\s?[a-zA-Z]+\s?[\.:]\s?(.+)',
message)
if matcher:
step, msg = matcher.groups()
return int(step), msg
return None, message
def run_proc(func, *args):
proc = multiprocessing.Process(
target=func,
args=args)
proc.daemon = True
proc.start()
return proc
def get_module(module_path):
pass

View File

@ -0,0 +1,13 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,49 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
sqlalchemy.url = postgresql+psycopg2://ostf:ostf@localhost/ostf
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -0,0 +1,32 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 logging
from alembic import command, config
from pecan import conf
log = logging.getLogger(__name__)
def do_apply_migrations():
alembic_conf = config.Config(
os.path.join(os.path.dirname(__file__), 'alembic.ini')
)
alembic_conf.set_main_option('script_location',
'fuel_plugin.ostf_adapter.storage:migrations')
alembic_conf.set_main_option('sqlalchemy.url', conf.dbpath)
command.upgrade(alembic_conf, 'head')

View File

@ -0,0 +1,57 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 pecan import conf
from sqlalchemy import create_engine, orm, pool
_ENGINE = None
_MAKER = None
def get_session(autocommit=True, expire_on_commit=False):
"""Return a SQLAlchemy session."""
global _MAKER
global _SLAVE_MAKER
maker = _MAKER
if maker is None:
engine = get_engine()
maker = get_maker(engine, autocommit, expire_on_commit)
else:
_MAKER = maker
session = maker()
return session
def get_engine(pool_type=None):
"""Return a SQLAlchemy engine."""
global _ENGINE
engine = _ENGINE
if engine is None:
engine = create_engine(conf.dbpath,
poolclass=pool_type or pool.NullPool)
_ENGINE = engine
return engine
def get_maker(engine, autocommit=True, expire_on_commit=False):
"""Return a SQLAlchemy sessionmaker using the given engine."""
return orm.sessionmaker(
bind=engine,
autocommit=autocommit,
expire_on_commit=expire_on_commit)

View File

@ -0,0 +1,42 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 json
from sqlalchemy.types import TypeDecorator, VARCHAR
class JsonField(TypeDecorator):
impl = VARCHAR
def process_bind_param(self, value, dialect):
if value is not None:
value = json.dumps(value)
return value
def process_result_value(self, value, dialect):
if value is not None:
value = json.loads(value)
return value
class ListField(JsonField):
def process_bind_param(self, value, dialect):
value = list(value) if value else []
super(ListField, self).process_bind_param(value, dialect)
def process_result_value(self, value, dialect):
value = super(ListField, self).process_bind_param(value, dialect)
return list(value) if value else []

View File

@ -0,0 +1 @@
Generic single-database configuration.

View File

@ -0,0 +1,13 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,90 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 __future__ import with_statement
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context
from fuel_plugin.ostf_adapter.storage import models
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = models.BASE.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
engine = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
connection = engine.connect()
context.configure(
connection=connection,
target_metadata=target_metadata
)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,22 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,42 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 status field to test run model
Revision ID: 12340edd992d
Revises: 1e2c38f575fb
Create Date: 2013-07-03 17:38:42.632146
"""
# revision identifiers, used by Alembic.
revision = '12340edd992d'
down_revision = '1e2c38f575fb'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column(
'test_runs',
sa.Column('status', sa.String(length=128), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('test_runs', 'status')
### end Alembic commands ###

View File

@ -0,0 +1,94 @@
"""Database refactoring
Revision ID: 1b28f7bc6476
Revises: 4e9905279776
Create Date: 2013-08-07 13:20:06.373200
"""
# revision identifiers, used by Alembic.
from fuel_plugin.ostf_adapter.storage import fields
revision = '1b28f7bc6476'
down_revision = '4e9905279776'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('test_runs', sa.Column('test_set_id', sa.String(length=128),
nullable=True))
op.add_column('test_runs',
sa.Column('meta', fields.JsonField(), nullable=True))
op.add_column('test_runs',
sa.Column('cluster_id', sa.Integer(), nullable=False))
op.drop_column('test_runs', u'type')
op.drop_column('test_runs', u'stats')
op.drop_column('test_runs', u'external_id')
op.drop_column('test_runs', u'data')
op.alter_column('test_runs', 'status',
existing_type=sa.VARCHAR(length=128),
nullable=False)
op.add_column('test_sets', sa.Column('cleanup_path', sa.String(length=128),
nullable=True))
op.add_column('test_sets',
sa.Column('meta', fields.JsonField(), nullable=True))
op.add_column('test_sets',
sa.Column('driver', sa.String(length=128), nullable=True))
op.add_column('test_sets',
sa.Column('additional_arguments', fields.ListField(),
nullable=True))
op.add_column('test_sets',
sa.Column('test_path', sa.String(length=256), nullable=True))
op.drop_column('test_sets', u'data')
op.add_column('tests', sa.Column('description', sa.Text(), nullable=True))
op.add_column('tests', sa.Column('traceback', sa.Text(), nullable=True))
op.add_column('tests', sa.Column('step', sa.Integer(), nullable=True))
op.add_column('tests', sa.Column('meta', fields.JsonField(),
nullable=True))
op.add_column('tests',
sa.Column('duration', sa.String(length=512), nullable=True))
op.add_column('tests', sa.Column('message', sa.Text(), nullable=True))
op.add_column('tests',
sa.Column('time_taken', sa.Float(), nullable=True))
op.drop_column('tests', u'taken')
op.drop_column('tests', u'data')
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('tests', sa.Column(u'data', sa.TEXT(), nullable=True))
op.add_column('tests', sa.Column(u'taken',
postgresql.DOUBLE_PRECISION(precision=53),
nullable=True))
op.drop_column('tests', 'time_taken')
op.drop_column('tests', 'message')
op.drop_column('tests', 'duration')
op.drop_column('tests', 'meta')
op.drop_column('tests', 'step')
op.drop_column('tests', 'traceback')
op.drop_column('tests', 'description')
op.add_column('test_sets', sa.Column(u'data', sa.TEXT(), nullable=True))
op.drop_column('test_sets', 'test_path')
op.drop_column('test_sets', 'additional_arguments')
op.drop_column('test_sets', 'driver')
op.drop_column('test_sets', 'meta')
op.drop_column('test_sets', 'cleanup_path')
op.alter_column('test_runs', 'status',
existing_type=sa.VARCHAR(length=128),
nullable=True)
op.add_column('test_runs', sa.Column(u'data', sa.TEXT(), nullable=True))
op.add_column('test_runs',
sa.Column(u'external_id', sa.VARCHAR(length=128),
nullable=True))
op.add_column('test_runs', sa.Column(u'stats', sa.TEXT(), nullable=True))
op.add_column('test_runs',
sa.Column(u'type', sa.VARCHAR(length=128), nullable=True))
op.drop_column('test_runs', 'cluster_id')
op.drop_column('test_runs', 'meta')
op.drop_column('test_runs', 'test_set_id')
### end Alembic commands ###

View File

@ -0,0 +1,54 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.
"""Storage refactor
Revision ID: 1e2c38f575fb
Revises: 1fcb29d29e03
Create Date: 2013-07-02 22:43:00.844574
"""
# revision identifiers, used by Alembic.
revision = '1e2c38f575fb'
down_revision = '1fcb29d29e03'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table(
'test_sets',
sa.Column('id', sa.String(length=128), nullable=False),
sa.Column('description', sa.String(length=128), nullable=True),
sa.Column('data', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.add_column(u'test_runs', sa.Column('external_id', sa.String(length=128),
nullable=True))
op.add_column(u'test_runs', sa.Column('stats', sa.Text(), nullable=True))
op.add_column(u'tests', sa.Column('test_set_id', sa.String(length=128),
nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column(u'tests', 'test_set_id')
op.drop_column(u'test_runs', 'stats')
op.drop_column(u'test_runs', 'external_id')
op.drop_table('test_sets')
### end Alembic commands ###

View File

@ -0,0 +1,60 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.
"""initial migration
Revision ID: 1fcb29d29e03
Revises: None
Create Date: 2013-06-26 17:40:23.908062
"""
# revision identifiers, used by Alembic.
revision = '1fcb29d29e03'
down_revision = None
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table(
'test_runs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('type', sa.String(length=128), nullable=True),
sa.Column('data', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table(
'tests',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=512), nullable=True),
sa.Column('status', sa.String(length=128), nullable=True),
sa.Column('taken', sa.Float(), nullable=True),
sa.Column('data', sa.Text(), nullable=True),
sa.Column('test_run_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['test_run_id'], ['test_runs.id'], ),
sa.PrimaryKeyConstraint('id')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('tests')
op.drop_table('test_runs')
### end Alembic commands ###

View File

@ -0,0 +1,27 @@
"""add title column
Revision ID: 3e45add6471
Revises: 1b28f7bc6476
Create Date: 2013-08-07 17:01:59.306413
"""
# revision identifiers, used by Alembic.
revision = '3e45add6471'
down_revision = '1b28f7bc6476'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('tests', sa.Column('title', sa.String(length=512),
nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('tests', 'title')
### end Alembic commands ###

View File

@ -0,0 +1,44 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 started_at , ended_at on TestRun model
Revision ID: 4e9905279776
Revises: 12340edd992d
Create Date: 2013-07-04 12:10:49.219213
"""
# revision identifiers, used by Alembic.
revision = '4e9905279776'
down_revision = '12340edd992d'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('test_runs',
sa.Column('started_at', sa.DateTime(), nullable=True))
op.add_column('test_runs',
sa.Column('ended_at', sa.DateTime(), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('test_runs', 'ended_at')
op.drop_column('test_runs', 'started_at')
### end Alembic commands ###

View File

@ -0,0 +1 @@
__author__ = 'tleontovich'

View File

@ -0,0 +1,275 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 datetime import datetime
import sqlalchemy as sa
from sqlalchemy import desc
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import joinedload, relationship, object_mapper
from fuel_plugin.ostf_adapter import nose_plugin
from fuel_plugin.ostf_adapter.storage import fields, engine
BASE = declarative_base()
class TestRun(BASE):
__tablename__ = 'test_runs'
STATES = (
'running',
'finished'
)
id = sa.Column(sa.Integer(), primary_key=True)
cluster_id = sa.Column(sa.Integer(), nullable=False)
status = sa.Column(sa.Enum(*STATES, name='test_run_states'),
nullable=False)
meta = sa.Column(fields.JsonField())
started_at = sa.Column(sa.DateTime, default=datetime.utcnow)
ended_at = sa.Column(sa.DateTime)
test_set_id = sa.Column(sa.String(128), sa.ForeignKey('test_sets.id'))
test_set = relationship('TestSet', backref='test_runs')
tests = relationship('Test', backref='test_run', order_by='Test.name')
def update(self, session, status):
self.status = status
if status == 'finished':
self.ended_at = datetime.utcnow()
session.add(self)
@property
def enabled_tests(self):
return [test.name for test in self.tests if test.status != 'disabled']
def is_finished(self):
return self.status == 'finished'
@property
def frontend(self):
test_run_data = {
'id': self.id,
'testset': self.test_set_id,
'meta': self.meta,
'cluster_id': self.cluster_id,
'status': self.status,
'started_at': self.started_at,
'ended_at': self.ended_at,
'tests': []
}
if self.tests:
test_run_data['tests'] = [test.frontend for test in self.tests]
return test_run_data
@classmethod
def add_test_run(cls, session, test_set, cluster_id, status='running',
tests=None):
predefined_tests = tests or []
tests = session.query(Test).filter_by(
test_set_id=test_set, test_run_id=None)
test_run = cls(test_set_id=test_set, cluster_id=cluster_id,
status=status)
session.add(test_run)
for test in tests:
session.add(test.copy_test(test_run, predefined_tests))
return test_run
@classmethod
def get_last_test_run(cls, session, test_set, cluster_id):
test_run = session.query(cls). \
filter_by(cluster_id=cluster_id, test_set_id=test_set). \
order_by(desc(cls.id)).first()
return test_run
@classmethod
def get_test_results(cls):
session = engine.get_session()
test_runs = session.query(cls). \
options(joinedload('tests')). \
order_by(desc(cls.id))
session.commit()
session.close()
return test_runs
@classmethod
def get_test_run(cls, session, test_run_id, joined=False):
if not joined:
test_run = session.query(cls). \
filter_by(id=test_run_id).first()
else:
test_run = session.query(cls). \
options(joinedload('tests')). \
filter_by(id=test_run_id).first()
return test_run
@classmethod
def update_test_run(cls, session, test_run_id, status=None):
updated_data = {}
if status:
updated_data['status'] = status
if status in ['finished']:
updated_data['ended_at'] = datetime.utcnow()
session.query(cls). \
filter(cls.id == test_run_id). \
update(updated_data, synchronize_session=False)
@classmethod
def is_last_running(cls, session, test_set, cluster_id):
test_run = cls.get_last_test_run(session, test_set, cluster_id)
return not bool(test_run) or test_run.is_finished()
@classmethod
def start(cls, session, test_set, metadata, tests):
plugin = nose_plugin.get_plugin(test_set.driver)
if cls.is_last_running(session, test_set.id,
metadata['cluster_id']):
test_run = cls.add_test_run(
session, test_set.id,
metadata['cluster_id'], tests=tests)
plugin.run(test_run, test_set)
return test_run.frontend
return {}
def restart(self, session, tests=None):
"""Restart test run with
if tests given they will be enabled
"""
if TestRun.is_last_running(session,
self.test_set_id,
self.cluster_id):
plugin = nose_plugin.get_plugin(self.test_set.driver)
self.update(session, 'running')
if tests:
Test.update_test_run_tests(
session, self.id, tests)
plugin.run(self, self.test_set, tests)
return self.frontend
return {}
def stop(self, session):
"""Stop test run if running
"""
plugin = nose_plugin.get_plugin(self.test_set.driver)
killed = plugin.kill(
self.id, self.cluster_id,
cleanup=self.test_set.cleanup_path)
if killed:
Test.update_running_tests(
session, self.id, status='stopped')
return self.frontend
class TestSet(BASE):
__tablename__ = 'test_sets'
id = sa.Column(sa.String(128), primary_key=True)
description = sa.Column(sa.String(256))
test_path = sa.Column(sa.String(256))
driver = sa.Column(sa.String(128))
additional_arguments = sa.Column(fields.ListField())
cleanup_path = sa.Column(sa.String(128))
meta = sa.Column(fields.JsonField())
tests = relationship('Test',
backref='test_set', order_by='Test.name')
@property
def frontend(self):
return {'id': self.id, 'name': self.description}
@classmethod
def get_test_set(cls, session, test_set):
return session.query(cls).filter_by(id=test_set).first()
class Test(BASE):
__tablename__ = 'tests'
STATES = (
'wait_running',
'running',
'failure',
'success',
'error',
'stopped'
)
id = sa.Column(sa.Integer(), primary_key=True)
name = sa.Column(sa.String(512))
title = sa.Column(sa.String(512))
description = sa.Column(sa.Text())
duration = sa.Column(sa.String(512))
message = sa.Column(sa.Text())
traceback = sa.Column(sa.Text())
status = sa.Column(sa.Enum(*STATES, name='test_states'))
step = sa.Column(sa.Integer())
time_taken = sa.Column(sa.Float())
meta = sa.Column(fields.JsonField())
test_set_id = sa.Column(sa.String(128), sa.ForeignKey('test_sets.id'))
test_run_id = sa.Column(sa.Integer(), sa.ForeignKey('test_runs.id'))
@property
def frontend(self):
return {
'id': self.name,
'testset': self.test_set_id,
'name': self.title,
'description': self.description,
'duration': self.duration,
'message': self.message,
'step': self.step,
'status': self.status,
'taken': self.time_taken
}
@classmethod
def add_result(cls, session, test_run_id, test_name, data):
session.query(cls).\
filter_by(name=test_name, test_run_id=test_run_id).\
update(data, synchronize_session=False)
@classmethod
def update_running_tests(cls, session, test_run_id, status='stopped'):
session.query(cls). \
filter(cls.test_run_id == test_run_id,
cls.status.in_(('running', 'wait_running'))). \
update({'status': status}, synchronize_session=False)
@classmethod
def update_test_run_tests(cls, session, test_run_id,
tests_names, status='wait_running'):
session.query(cls). \
filter(cls.name.in_(tests_names),
cls.test_run_id == test_run_id). \
update({'status': status}, synchronize_session=False)
def copy_test(self, test_run, predefined_tests):
new_test = self.__class__()
mapper = object_mapper(self)
primary_keys = set([col.key for col in mapper.primary_key])
for column in mapper.iterate_properties:
if column.key not in primary_keys:
setattr(new_test, column.key, getattr(self, column.key))
new_test.test_run_id = test_run.id
if predefined_tests and new_test.name not in predefined_tests:
new_test.status = 'disabled'
else:
new_test.status = 'wait_running'
return new_test

View File

@ -0,0 +1,25 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 fuel_plugin.ostf_adapter.storage import models
def update_all_running_test_runs(session):
session.query(models.TestRun). \
filter_by(status='running'). \
update({'status': 'finished'}, synchronize_session=False)
session.query(models.Test). \
filter(models.Test.status.in_(('running', 'wait_running'))). \
update({'status': 'stopped'}, synchronize_session=False)

View File

@ -0,0 +1,13 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,52 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 pecan
from fuel_plugin.ostf_adapter.wsgi import hooks
PECAN_DEFAULT = {
'server': {
'host': '0.0.0.0',
'port': 8989
},
'app': {
'root': 'fuel_plugin.ostf_adapter.wsgi.root.RootController',
'modules': ['fuel_plugin.ostf_adapter.wsgi']
},
'nailgun': {
'host': '127.0.0.1',
'port': 8000
},
'dbpath': 'postgresql+psycopg2://ostf:ostf@localhost/ostf',
'debug': False,
'debug_tests': 'functional/dummy_tests'
}
def setup_config(pecan_config):
pecan_config.update(PECAN_DEFAULT)
pecan.conf.update(pecan_config)
def setup_app(config=None):
setup_config(config or {})
app_hooks = [hooks.SessionHook()]
app = pecan.make_app(
pecan.conf.app.root,
debug=pecan.conf.debug,
force_canonical=True,
hooks=app_hooks
)
return app

View File

@ -0,0 +1,136 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 json
import logging
from sqlalchemy import func
from sqlalchemy.orm import joinedload
from pecan import rest, expose, request
from fuel_plugin.ostf_adapter.storage import models
LOG = logging.getLogger(__name__)
class BaseRestController(rest.RestController):
def _handle_get(self, method, remainder):
if len(remainder):
method_name = remainder[0]
if method.upper() in self._custom_actions.get(method_name, []):
controller = self._find_controller(
'get_%s' % method_name,
method_name
)
if controller:
return controller, remainder[1:]
return super(BaseRestController, self)._handle_get(method, remainder)
class TestsController(BaseRestController):
@expose('json')
def get_one(self, test_name):
raise NotImplementedError()
@expose('json')
def get_all(self):
with request.session.begin(subtransactions=True):
return [item.frontend for item
in request.session.query(models.Test).all()]
class TestsetsController(BaseRestController):
@expose('json')
def get_one(self, test_set):
with request.session.begin(subtransactions=True):
test_set = request.session.query(models.TestSet)\
.filter_by(id=test_set).first()
if test_set and isinstance(test_set, models.TestSet):
return test_set.frontend
return {}
@expose('json')
def get_all(self):
with request.session.begin(subtransactions=True):
return [item.frontend for item
in request.session.query(models.TestSet).all()]
class TestrunsController(BaseRestController):
_custom_actions = {
'last': ['GET'],
}
@expose('json')
def get_all(self):
with request.session.begin(subtransactions=True):
return [item.frontend for item
in request.session.query(models.TestRun).all()]
@expose('json')
def get_one(self, test_run_id):
with request.session.begin(subtransactions=True):
test_run = request.session.query(models.TestRun)\
.filter_by(id=test_run_id).first()
if test_run and isinstance(test_run, models.TestRun):
return test_run.frontend
return {}
@expose('json')
def get_last(self, cluster_id):
with request.session.begin(subtransactions=True):
test_run_ids = request.session.query(func.max(models.TestRun.id)) \
.group_by(models.TestRun.test_set_id).\
filter_by(cluster_id=cluster_id)
test_runs = request.session.query(models.TestRun). \
options(joinedload('tests')). \
filter(models.TestRun.id.in_(test_run_ids))
return [item.frontend for item in test_runs]
@expose('json')
def post(self):
test_runs = json.loads(request.body)
res = []
with request.session.begin(subtransactions=True):
for test_run in test_runs:
test_set = test_run['testset']
metadata = test_run['metadata']
tests = test_run.get('tests', [])
test_set = models.TestSet.get_test_set(
request.session, test_set)
test_run = models.TestRun.start(
request.session, test_set, metadata, tests)
res.append(test_run)
return res
@expose('json')
def put(self):
test_runs = json.loads(request.body)
data = []
with request.session.begin(subtransactions=True):
for test_run in test_runs:
status = test_run.get('status')
tests = test_run.get('tests', [])
test_run = models.TestRun.get_test_run(request.session,
test_run['id'])
if status == 'stopped':
data.append(test_run.stop(request.session))
elif status == 'restarted':
data.append(test_run.restart(request.session, tests=tests))
return data

View File

@ -0,0 +1,55 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 logging
from stevedore import extension
from pecan import hooks
from fuel_plugin.ostf_adapter.storage import engine
LOG = logging.getLogger(__name__)
class ExceptionHandlingHook(hooks.PecanHook):
def on_error(self, state, e):
LOG.exception('Pecan state %s', state)
# class StorageHook(hooks.PecanHook):
# def __init__(self):
# super(StorageHook, self).__init__()
# self.storage = storage.get_storage()
#
# def before(self, state):
# state.request.storage = self.storage
class PluginsHook(hooks.PecanHook):
PLUGINS_NAMESPACE = 'plugins'
def __init__(self):
super(PluginsHook, self).__init__()
self.plugin_manager = extension.ExtensionManager(
self.PLUGINS_NAMESPACE, invoke_on_load=True)
def before(self, state):
state.request.plugin_manager = self.plugin_manager
class SessionHook(hooks.PecanHook):
def before(self, state):
state.request.session = engine.get_session()

View File

@ -0,0 +1,34 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 pecan import expose
from fuel_plugin.ostf_adapter.wsgi import controllers
class V1Controller(object):
"""
TODO Rewrite it with wsme expose
"""
tests = controllers.TestsController()
testsets = controllers.TestsetsController()
testruns = controllers.TestrunsController()
class RootController(object):
v1 = V1Controller()
@expose('json', generic=True)
def index(self):
return {}

View File

@ -0,0 +1,13 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,132 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 requests
from json import dumps
import time
class TestingAdapterClient(object):
def __init__(self, url):
self.url = url
def _request(self, method, url, data=None):
headers = {'content-type': 'application/json'}
r = requests.request(method, url, data=data, headers=headers, timeout=30.0)
if 2 != r.status_code/100:
raise AssertionError('{method} "{url}" responded with '
'"{code}" status code'.format(
method=method.upper(),
url=url, code=r.status_code))
return r
def __getattr__(self, item):
getters = ['testsets', 'tests', 'testruns']
if item in getters:
url = ''.join([self.url, '/', item])
return lambda: self._request('GET', url)
def testruns_last(self, cluster_id):
url = ''.join([self.url, '/testruns/last/',
str(cluster_id)])
return self._request('GET', url)
def start_testrun(self, testset, cluster_id):
return self.start_testrun_tests(testset, [], cluster_id)
def start_testrun_tests(self, testset, tests, cluster_id):
url = ''.join([self.url, '/testruns'])
data = [{'testset': testset,
'tests': tests,
'metadata': {'cluster_id': str(cluster_id)}}]
return self._request('POST', url, data=dumps(data))
def stop_testrun(self, testrun_id):
url = ''.join([self.url, '/testruns'])
data = [{"id": testrun_id,
"status": "stopped"}]
return self._request("PUT", url, data=dumps(data))
def stop_testrun_last(self, testset, cluster_id):
latest = self.testruns_last(cluster_id).json()
testrun_id = [item['id'] for item in latest
if item['testset'] == testset][0]
return self.stop_testrun(testrun_id)
def restart_tests(self, tests, testrun_id):
url = ''.join([self.url, '/testruns'])
body = [{'id': str(testrun_id),
'tests': tests,
'status': 'restarted'}]
return self._request('PUT', url, data=dumps(body))
def restart_tests_last(self, testset, tests, cluster_id):
latest = self.testruns_last(cluster_id).json()
testrun_id = [item['id'] for item in latest
if item['testset'] == testset][0]
return self.restart_tests(tests, testrun_id)
def _with_timeout(self, action, testset, cluster_id,
timeout, polling=5, polling_hook=None):
start_time = time.time()
json = action().json()
if json == [{}]:
self.stop_testrun_last(testset, cluster_id)
time.sleep(1)
action()
while time.time() - start_time <= timeout:
time.sleep(polling)
current_response = self.testruns_last(cluster_id)
if polling_hook:
polling_hook(current_response)
current_status, current_tests = \
[(item['status'], item['tests']) for item
in current_response.json() if item['testset'] == testset][0]
if current_status == 'finished':
break
else:
stopped_response = self.stop_testrun_last(testset, cluster_id)
if polling_hook:
polling_hook(stopped_response)
stopped_response = self.testruns_last(cluster_id)
stopped_status = [item['status'] for item in stopped_response.json()
if item['testset'] == testset][0]
msg = '{0} is still in {1} state. Now the state is {2}'.format(
testset, current_status, stopped_status)
msg_tests = '\n'.join(['{0} -> {1}, {2}'.format(
item['id'], item['status'], item['taken'])
for item in current_tests])
raise AssertionError('\n'.join([msg, msg_tests]))
return current_response
def run_with_timeout(self, testset, tests, cluster_id, timeout, polling=5,
polling_hook=None):
action = lambda: self.start_testrun_tests(testset, tests, cluster_id)
return self._with_timeout(action, testset, cluster_id, timeout,
polling, polling_hook)
def run_testset_with_timeout(self, testset, cluster_id, timeout,
polling=5, polling_hook=None):
return self.run_with_timeout(testset, [], cluster_id, timeout,
polling, polling_hook)
def restart_with_timeout(self, testset, tests, cluster_id, timeout):
action = lambda: self.restart_tests_last(testset, tests, cluster_id)
return self._with_timeout(action, testset, cluster_id, timeout)

162
fuel_plugin/ostf_client/ostf.py Executable file
View File

@ -0,0 +1,162 @@
#!/usr/bin/env python
"""Openstack testing framework client
Usage: ostf.py run <test_set> [-q] [--id=<cluster_id>] [--tests=<tests>] [--url=<url>] [--timeout=<timeout>]
ostf.py list [<test_set>]
-q Show test run result only after finish
-h --help Show this screen
--tests=<tests> Tests to run
--id=<cluster_id> Cluster id to use, default: OSTF_CLUSTER_ID or "1"
--url=<url> Ostf url, default: OSTF_URL or http://0.0.0.0:8989/v1
--timeout=<timeout> Amount of time after which test_run will be stopped [default: 60]
"""
import os
import sys
from requests import get
from docopt import docopt
from clint.textui import puts, colored, columns, indent
from blessings import Terminal
from fuel_plugin.ostf_client.client import TestingAdapterClient
def get_cluster_id():
try:
r = get('http://localhost:8000/api/clusters').json()
except:
return 0
return next(item['id'] for item in r)
def main():
t = Terminal()
args = docopt(__doc__, version='0.1')
test_set = args['<test_set>']
cluster_id = args['--id'] or os.environ.get('OSTF_CLUSTER_ID') \
or get_cluster_id() or '1'
tests = args['--tests'] or []
timeout = args['--timeout']
quite = args['-q']
url = args['--url'] or os.environ.get('OSTF_URL') \
or 'http://0.0.0.0:8989/v1'
client = TestingAdapterClient(url)
def run():
col = 60
statused = dict(
running='running',
wait_running='wait_running',
success=colored.green('success'),
finished=colored.blue('finished'),
failure=colored.red('failure'),
error=colored.red('error'),
stopped=colored.red('stopped')
)
tests = [test['id'].split('.')[-1]
for test in client.tests().json()
if test['testset'] == test_set]
def print_results(item):
if isinstance(item, dict):
puts(columns([item['id'].split('.')[-1], col],
[statused[item['status']], col]))
else:
puts(columns([item[0], col],
[statused.get(item[1], item[1]), col]))
def move_up(lines):
for _ in range(lines):
print t.move_up + t.move_left,
def polling_hook(response):
current_status, current_tests = next(
(item['status'], item['tests']) for item in response.json()
if item['testset'] == test_set)
move_up(len(current_tests) + 1)
for test in current_tests:
print_results(test)
print_results(['General', current_status])
def quite_polling_hook(response):
if not quite_polling_hook.__dict__.get('published_tests'):
quite_polling_hook.__dict__['published_tests'] = []
current_status, current_tests = next(
(item['status'], item['tests']) for item in response.json()
if item['testset'] == test_set)
finished_statuses = ['success', 'failure', 'stopped', 'error']
finished_tests = [item for item in current_tests
if item['status'] in finished_statuses
and item
not in quite_polling_hook.__dict__['published_tests']]
for test in finished_tests:
print_results(test)
quite_polling_hook.__dict__['published_tests'].append(test)
if current_status == 'finished':
print_results(['General', current_status])
if quite:
polling_hook = quite_polling_hook
else:
for test in tests:
print_results([test, 'wait_running'])
print_results(['General', 'running'])
try:
r = client.run_testset_with_timeout(test_set, cluster_id,
timeout, 2, polling_hook)
except AssertionError as e:
return 1
except KeyboardInterrupt as e:
r = client.stop_testrun_last(test_set, cluster_id)
print t.move_left + t.move_left,
polling_hook(r)
tests = next(item['tests'] for item in r.json())
return any(item['status'] != 'success' for item in tests)
def list_tests():
result = client.tests().json()
tests = (test for test in result if test['testset'] == test_set)
col = 60
puts(columns([(colored.red("ID")), col],
[(colored.red("NAME")), None]))
for test in tests:
test_id = test['id'].split('.')[-1]
puts(columns([test_id, col], [test['name'], None]))
return 0
def list_test_sets():
result = client.testsets().json()
col = 60
puts(columns([(colored.red("ID")), col],
[(colored.red("NAME")), None]))
for test_set in result:
puts(columns([test_set['id'], col], [test_set['name'], None]))
return 0
if args['run']:
return run()
if test_set:
return list_tests()
return list_test_sets()
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,13 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,15 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,134 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 functools import wraps
from unittest import TestCase
from fuel_plugin.ostf_client.client import TestingAdapterClient
class EmptyResponseError(Exception):
pass
class Response(object):
"""This is testing_adapter response object"""
test_name_mapping = {}
def __init__(self, response):
self.is_empty = False
if isinstance(response, list):
self._parse_json(response)
self.request = None
else:
self._parse_json(response.json())
self.request = '{0} {1} \n with {2}'\
.format(response.request.method, response.request.url, response.request.body)
def __getattr__(self, item):
if item in self.test_sets or item in self._tests:
return self.test_sets.get(item) or self._tests.get(item)
else:
return super(type(self), self).__delattr__(item)
def __str__(self):
if self.is_empty:
return "Empty"
return self.test_sets.__str__()
@classmethod
def set_test_name_mapping(cls, mapping):
cls.test_name_mapping = mapping
def _parse_json(self, json):
if json == [{}]:
self.is_empty = True
return
else:
self.is_empty = False
self.test_sets = {}
self._tests = {}
for testset in json:
self.test_sets[testset.pop('testset')] = testset
self._tests = dict((self._friendly_name(item.get('id')), item) for item in testset['tests'])
def _friendly_name(self, name):
return self.test_name_mapping.get(name, name)
class AdapterClientProxy(object):
def __init__(self, url):
self.client = TestingAdapterClient(url)
def __getattr__(self, item):
if item in TestingAdapterClient.__dict__:
call = getattr(self.client, item)
return self._decorate_call(call)
def _friendly_map(self, mapping):
Response.set_test_name_mapping(mapping)
def _decorate_call(self, call):
@wraps(call)
def inner(*args, **kwargs):
r = call(*args, **kwargs)
return Response(r)
return inner
class SubsetException(Exception):
pass
class BaseAdapterTest(TestCase):
def compare(self, response, comparable):
if response.is_empty:
msg = '{0} is empty'.format(response.request)
raise AssertionError(msg)
if not isinstance(comparable, Response):
comparable = Response(comparable)
test_set = comparable.test_sets.keys()[0]
test_set_data = comparable.test_sets[test_set]
tests = comparable._tests
diff = []
for item in test_set_data:
if item == 'tests':
continue
if response.test_sets[test_set][item] != test_set_data[item]:
msg = 'Actual "{0}" != expected "{1}" in {2}.{3}'.format(response.test_sets[test_set][item],
test_set_data[item], test_set, item)
diff.append(msg)
for test_name, test in tests.iteritems():
for t in test:
if t == 'id':
continue
if response._tests[test_name][t] != test[t]:
msg = 'Actual "{0}" != expected"{1}" in {2}.{3}.{4}'.format(response._tests[test_name][t],
test[t], test_set, test_name, t)
diff.append(msg)
if diff:
raise AssertionError(diff)
@staticmethod
def init_client(url, mapping):
ac = AdapterClientProxy(url)
ac._friendly_map(mapping)
return ac

View File

@ -0,0 +1,107 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.
CONFIG = {'compute-admin_password': 'nova',
'compute-admin_tenant_name': '',
'compute-admin_username': '',
'compute_allow_tenant_isolation': 'True',
'compute_allow_tenant_reuse': 'true',
'compute_block_migrate_supports_cinder_iscsi': 'false',
'compute_build_interval': '3',
'compute_build_timeout': '300',
'compute_catalog_type': 'compute',
'compute_change_password_available': 'False',
'compute_controller_node': '10.30.1.101',
'compute_controller_node_name': 'fuel-controller-01.localdomain.',
'compute_controller_node_ssh_password': 'r00tme',
'compute_controller_node_ssh_user': 'root',
'compute_create_image_enabled': 'true',
'compute_disk_config_enabled_override': 'true',
'compute_enabled_services': 'nova-cert, nova-consoleauth, nova-scheduler, nova-conductor, nova-cert, nova-consoleauth, nova-scheduler, nova-conductor, nova-cert, nova-consoleauth, nova-scheduler, nova-conductor, nova-compute',
'compute_fixed_network_name': 'private',
'compute_flavor_ref': '1',
'compute_flavor_ref_alt': '2',
'compute_image_alt_ssh_user': 'cirros',
'compute_image_ref': '53734a0d-60a8-4689-b7c8-3c14917a7197',
'compute_image_ref_alt': '53734a0d-60a8-4689-b7c8-3c14917a7197',
'compute_image_ssh_user': 'cirros',
'compute_ip_version_for_ssh': '4',
'compute_live_migration_available': 'False',
'compute_network_for_ssh': 'private',
'compute_resize_available': 'true',
'compute_run_ssh': 'false',
'compute_ssh_channel_timeout': '60',
'compute_ssh_timeout': '300',
'compute_ssh_user': 'cirros',
'compute_use_block_migration_for_live_migration': 'False',
'identity_admin_password': 'nova',
'identity_admin_tenant_name': 'admin',
'identity_admin_username': 'admin',
'identity_alt_password': 'nova',
'identity_alt_tenant_name': 'alt_demo',
'identity_alt_username': 'alt_demo',
'identity_catalog_type': 'identity',
'identity_disable_ssl_certificate_validation': 'False',
'identity_password': 'nova',
'identity_region': 'RegionOne',
'identity_strategy': 'keystone',
'identity_tenant_name': 'admin',
'identity_uri': 'http://172.18.164.70:5000/v2.0/',
'identity_url': 'http://172.18.164.70/',
'identity_username': 'admin',
'image_api_version': '1',
'image_catalog_type': 'image',
'image_http_image': 'http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-uec.tar.gz',
'network_api_version': '2.0',
'network_catalog_type': 'network',
'network_public_network_id': 'cdb94175-2002-449f-be41-6b8afce8de13',
'network_public_router_id': '2a6bf65b-01f7-4c91-840a-2b5f676e7016',
'network_quantum_available': 'true',
'network_tenant_network_cidr': '10.13.0.0/16',
'network_tenant_network_mask_bits': '28',
'network_tenant_networks_reachable': 'true',
'object-storage_catalog_type': 'object-store',
'object-storage_container_sync_interval': '5',
'object-storage_container_sync_timeout': '120',
'smoke_allow_tenant_isolation': 'True',
'smoke_allow_tenant_reuse': 'true',
'smoke_block_migrate_supports_cinder_iscsi': 'false',
'smoke_build_interval': '3',
'smoke_build_timeout': '300',
'smoke_catalog_type': 'compute',
'smoke_change_password_available': 'False',
'smoke_create_image_enabled': 'true',
'smoke_disk_config_enabled_override': 'true',
'smoke_fixed_network_name': 'net04',
'smoke_flavor_ref': '1',
'smoke_flavor_ref_alt': '2',
'smoke_image_alt_ssh_user': 'cirros',
'smoke_image_ref': '53734a0d-60a8-4689-b7c8-3c14917a7197',
'smoke_image_ref_alt': '53734a0d-60a8-4689-b7c8-3c14917a7197',
'smoke_image_ssh_user': 'cirros',
'smoke_ip_version_for_ssh': '4',
'smoke_live_migration_available': 'False',
'smoke_network_for_ssh': 'net04',
'smoke_resize_available': 'true',
'smoke_run_ssh': 'false',
'smoke_ssh_channel_timeout': '60',
'smoke_ssh_timeout': '320',
'smoke_ssh_user': 'cirros',
'smoke_use_block_migration_for_live_migration': 'False',
'volume_backend1_name': 'BACKEND_1',
'volume_backend2_name': 'BACKEND_2',
'volume_build_interval': '3',
'volume_build_timeout': '300',
'volume_catalog_type': 'volume',
'volume_multi_backend_enabled': 'false'}

View File

@ -0,0 +1,13 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,26 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 os import environ as env
from unittest import TestCase
from oslo.config import cfg
opts = [
cfg.StrOpt('quantum', default='fake')
]
class Config(TestCase):
def test_config(self):
file_path = env['OSTF_CONF_PATH']
cfg.CONF

View File

@ -0,0 +1,59 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.
__profile__ = {
"id": "general_test",
"driver": "nose",
"test_path": "fuel_plugin/tests/functional/dummy_tests/general_test.py",
"description": "General fake tests"
}
import time
import httplib
import unittest
class Dummy_test(unittest.TestCase):
"""Class docstring is required?
"""
def test_fast_pass(self):
"""fast pass test
This is a simple always pass test
Duration: 1sec
"""
self.assertTrue(True)
def test_long_pass(self):
"""Will sleep 5 sec
This is a simple test
it will run for 5 sec
Duration: 5sec
"""
time.sleep(5)
self.assertTrue(True)
def test_fast_fail(self):
"""Fast fail
"""
self.assertTrue(False, msg='Something goes wroooong')
def test_fast_error(self):
"""And fast error
"""
conn = httplib.HTTPSConnection('random.random/random')
conn.request("GET", "/random.aspx")
def test_fail_with_step(self):
self.fail('Step 3 Failed: Fake fail message')

View File

@ -0,0 +1,45 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.
__profile__ = {
"id": "stopped_test",
"driver": "nose",
"test_path": "fuel_plugin/tests/functional/dummy_tests/stopped_test.py",
"description": "Long running 25 secs fake tests"
}
import time
import unittest
class dummy_tests_stopped(unittest.TestCase):
def test_really_long(self):
"""This is long running tests
Duration: 25sec
"""
time.sleep(25)
self.assertTrue(True)
def test_one_no_so_long(self):
"""What i am doing here? You ask me????
"""
time.sleep(5)
self.assertFalse(1 == 2)
def test_not_long_at_all(self):
"""You know.. for testing
Duration: 1sec
"""
self.assertTrue(True)

View File

@ -0,0 +1,38 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 requests
import json
import time
import pprint
def make_requests(claster_id, test_set):
body = [{'testset': test_set,
'metadata': {'config': {'identity_uri': 'hommeee'},
'cluster_id': claster_id}
}
]
headers = {'Content-Type': 'application/json'}
response = requests.post('http://172.18.164.37:8777/v1/testruns', data=json.dumps(body), headers=headers)
pprint.pprint(response.json())
_id = response.json()[0]['id']
time.sleep(1)
body = [{'id': _id, 'status': 'stopped'}]
update = requests.put('http://172.18.164.37:8777/v1/testruns', data=json.dumps(body), headers=headers)
get_resp = requests.get('http://172.18.164.37:8777/v1/testruns/last/%s' % claster_id)
data = get_resp.json()
pprint.pprint(data)
if __name__ == '__main__':
make_requests(11, 'fuel_health')

View File

@ -0,0 +1,35 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 requests
import json
import time
import pprint
def make_requests(claster_id, test_set):
tests = ['functional.dummy_tests.general_test.Dummy_test.test_fast_pass',
'functional.dummy_tests.general_test.Dummy_test.test_fast_error']
body = [{'testset': test_set,
'tests': tests,
'metadata': {
'cluster_id': claster_id}}]
headers = {'Content-Type': 'application/json'}
response = requests.post('http://127.0.0.1:8989/v1/testruns',
data=json.dumps(body), headers=headers)
pprint.pprint(response.json())
if __name__ == '__main__':
make_requests('101', 'plugin_general')

View File

@ -0,0 +1,35 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 gevent
from gevent import monkey
monkey.patch_all()
import requests
import json
import time
import pprint
def make_requests(claster_id, test_set):
body = [{'testset': test_set,
'metadata': {'config': {},
'cluster_id': claster_id}}]
headers = {'Content-Type': 'application/json'}
response = requests.post('http://127.0.0.1:8989/v1/testruns',
data=json.dumps(body), headers=headers)
pprint.pprint(response.json())
if __name__ == '__main__':
make_requests('308', 'general_test')

View File

@ -0,0 +1,34 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 requests
import json
import time
import pprint
def make_requests(claster_id, test_set):
tests = ['fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_long_pass']
body = [{'id': claster_id,
'tests': tests,
'status': 'restarted',
}]
headers = {'Content-Type': 'application/json'}
response = requests.put('http://127.0.0.1:8989/v1/testruns',
data=json.dumps(body), headers=headers)
pprint.pprint(response.json())
if __name__ == '__main__':
make_requests(370, 'plugin_general')

View File

@ -0,0 +1,30 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 requests
import json
import time
import pprint
def make_requests(claster_id, test_set):
body = [{'id': claster_id, 'status': 'stopped'}]
headers = {'Content-Type': 'application/json'}
update = requests.put(
'http://localhost:8989/v1/testruns',
data=json.dumps(body), headers=headers)
data = update.json()
pprint.pprint(data)
if __name__ == '__main__':
make_requests(378, 'plugin_stopped')

View File

@ -0,0 +1,66 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 fuel_plugin.tests.functional.base import BaseAdapterTest
class ScenarioTests(BaseAdapterTest):
@classmethod
def setUpClass(cls):
url = 'http://0.0.0.0:8989/v1'
mapping = {}
cls.client = cls.init_client(url, mapping)
def test_random_scenario(self):
testset = "fuel_sanity"
cluster_id = 3
tests = []
timeout = 60
from pprint import pprint
for i in range(1):
r = self.client.run_with_timeout(testset, tests, cluster_id, timeout)
pprint([item for item in r.test_sets[testset]['tests']])
if r.fuel_sanity['status'] == 'stopped':
running_tests = [test for test in r._tests
if r._tests[test]['status'] is 'stopped']
print "restarting: ", running_tests
result = self.client.restart_with_timeout(testset, running_tests, cluster_id, timeout)
print 'Restart', result
def test_run_fuel_sanity(self):
testset = "fuel_sanity"
cluster_id = 3
tests = []
timeout = 240
r = self.client.run_with_timeout(testset, tests, cluster_id, timeout)
for item in r.fuel_sanity['tests']:
print item['id'].split('.').pop(), item
self.assertEqual(r.fuel_sanity['status'], 'finished')
def test_run_fuel_smoke(self):
testset = "fuel_smoke"
cluster_id = 3
tests = []
timeout = 900
r = self.client.run_with_timeout(testset, tests, cluster_id, timeout)
for item in r.fuel_sanity['tests']:
print item['id'].split('.').pop(), item
self.assertEqual(r.fuel_smoke['status'], 'finished')

View File

@ -0,0 +1,255 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 time
from fuel_plugin.tests.functional.base import BaseAdapterTest, Response
from fuel_plugin.ostf_client.client import TestingAdapterClient as adapter
class AdapterTests(BaseAdapterTest):
@classmethod
def setUpClass(cls):
url = 'http://0.0.0.0:8989/v1'
cls.mapping = {
'fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_pass': 'fast_pass',
'fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_error': 'fast_error',
'fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_fail': 'fast_fail',
'fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_long_pass': 'long_pass',
'fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fail_with_step': 'fail_step',
'fuel_plugin.tests.functional.dummy_tests.stopped_test.dummy_tests_stopped.test_really_long': 'really_long',
'fuel_plugin.tests.functional.dummy_tests.stopped_test.dummy_tests_stopped.test_not_long_at_all': 'not_long',
'fuel_plugin.tests.functional.dummy_tests.stopped_test.dummy_tests_stopped.test_one_no_so_long': 'so_long'
}
cls.testsets = {
# "fuel_smoke": None,
# "fuel_sanity": None,
"general_test": ['fast_pass', 'fast_error', 'fast_fail', 'long_pass'],
"stopped_test": ['really_long', 'not_long', 'so_long']
}
cls.adapter = adapter(url)
cls.client = cls.init_client(url, cls.mapping)
def test_list_testsets(self):
"""Verify that self.testsets are in json response
"""
json = self.adapter.testsets().json()
response_testsets = [item['id'] for item in json]
for testset in self.testsets:
msg = '"{test}" not in "{response}"'.format(test=testset, response=response_testsets)
self.assertTrue(testset in response_testsets, msg)
def test_list_tests(self):
"""Verify that self.tests are in json response
"""
json = self.adapter.tests().json()
response_tests = [item['id'] for item in json]
for test in self.mapping:
msg = '"{test}" not in "{response}"'.format(test=test.capitalize(), response=response_tests)
self.assertTrue(test in response_tests, msg)
def test_run_testset(self):
"""Verify that test status changes in time from running to success
"""
testset = "general_test"
cluster_id = 1
self.client.start_testrun(testset, cluster_id)
time.sleep(3)
r = self.client.testruns_last(cluster_id)
assertions = Response([{'status': 'running',
'testset': 'general_test',
'tests': [
{'id': 'fast_pass', 'status': 'success', 'name': 'fast pass test',
'description': """ This is a simple always pass test
""",},
{'id': 'long_pass', 'status': 'running'},
{'id': 'fail_step', 'message': 'MEssaasasas', 'status': 'failure'},
{'id': 'fast_error', 'message': '', 'status': 'error'},
{'id': 'fast_fail', 'message': 'Something goes wroooong', 'status': 'failure'}]}])
print r
print assertions
self.compare(r, assertions)
time.sleep(10)
r = self.client.testruns_last(cluster_id)
assertions.general_test['status'] = 'finished'
assertions.long_pass['status'] = 'success'
self.compare(r, assertions)
def test_stop_testset(self):
"""Verify that long running testrun can be stopped
"""
testset = "stopped_test"
cluster_id = 2
self.client.start_testrun(testset, cluster_id)
time.sleep(10)
r = self.client.testruns_last(cluster_id)
assertions = Response([
{'status': 'running',
'testset': 'stopped_test',
'tests': [
{'id': 'not_long', 'status': 'success'},
{'id': 'so_long', 'status': 'success'},
{'id': 'really_long', 'status': 'running'}]}])
self.compare(r, assertions)
self.client.stop_testrun_last(testset, cluster_id)
r = self.client.testruns_last(cluster_id)
assertions.stopped_test['status'] = 'finished'
assertions.really_long['status'] = 'stopped'
self.compare(r, assertions)
def test_cant_start_while_running(self):
"""Verify that you can't start new testrun for the same cluster_id while previous run is running"""
testsets = {"stopped_test": None,
"general_test": None}
cluster_id = 3
for testset in testsets:
self.client.start_testrun(testset, cluster_id)
self.client.testruns_last(cluster_id)
for testset in testsets:
r = self.client.start_testrun(testset, cluster_id)
msg = "Response {0} is not empty when you try to start testrun" \
" with testset and cluster_id that are already running".format(r)
self.assertTrue(r.is_empty, msg)
def test_start_many_runs(self):
"""Verify that you can start 20 testruns in a row with different cluster_id"""
testset = "general_test"
for cluster_id in range(100, 105):
r = self.client.start_testrun(testset, cluster_id)
msg = '{0} was empty'.format(r.request)
self.assertFalse(r.is_empty, msg)
'''TODO: Rewrite assertions to verity that all 5 testruns ended with appropriate status'''
def test_run_single_test(self):
"""Verify that you can run individual tests from given testset"""
testset = "general_test"
tests = ['fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_pass',
'fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_fail']
cluster_id = 50
r = self.client.start_testrun_tests(testset, tests, cluster_id)
assertions = Response([
{'status': 'running',
'testset': 'general_test',
'tests': [
{'status': 'disabled', 'id': 'fast_error'},
{'status': 'wait_running', 'id': 'fast_fail'},
{'status': 'wait_running', 'id': 'fast_pass'},
{'status': 'disabled', 'id': 'long_pass'}]}])
self.compare(r, assertions)
time.sleep(2)
r = self.client.testruns_last(cluster_id)
assertions.general_test['status'] = 'finished'
assertions.fast_fail['status'] = 'failure'
assertions.fast_pass['status'] = 'success'
self.compare(r, assertions)
def test_single_test_restart(self):
"""Verify that you restart individual tests for given testrun"""
testset = "general_test"
tests = ['fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_pass',
'fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_fail']
cluster_id = 60
self.client.run_testset_with_timeout(testset, cluster_id, 10)
r = self.client.restart_tests_last(testset, tests, cluster_id)
assertions = Response([
{'status': 'running',
'testset': 'general_test',
'tests': [
{'id': 'fast_pass', 'status': 'wait_running'},
{'id': 'long_pass', 'status': 'success'},
{'id': 'fast_error', 'status': 'error'},
{'id': 'fast_fail', 'status': 'wait_running'}]}])
self.compare(r, assertions)
time.sleep(5)
r = self.client.testruns_last(cluster_id)
assertions.general_test['status'] = 'finished'
assertions.fast_pass['status'] = 'success'
assertions.fast_fail['status'] = 'failure'
self.compare(r, assertions)
def test_restart_combinations(self):
"""Verify that you can restart both tests that ran and did not run during single test start"""
testset = "general_test"
tests = ['fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_pass',
'fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_fail']
disabled_test = ['functional.dummy_tests.general_test.Dummy_test.test_fast_error', ]
cluster_id = 70
self.client.run_with_timeout(testset, tests, cluster_id, 70)
self.client.restart_with_timeout(testset, tests, cluster_id, 10)
r = self.client.restart_tests_last(testset, disabled_test, cluster_id)
assertions = Response([
{'status': 'running',
'testset': 'general_test',
'tests': [
{'status': 'wait_running', 'id': 'fast_error'},
{'status': 'failure', 'id': 'fast_fail'},
{'status': 'success', 'id': 'fast_pass'},
{'status': 'disabled', 'id': 'long_pass'}]}])
print r
self.compare(r, assertions)
time.sleep(5)
r = self.client.testruns_last(cluster_id)
assertions.general_test['status'] = 'finished'
assertions.fast_error['status'] = 'error'
self.compare(r, assertions)
def test_cant_restart_during_run(self):
testset = 'general_test'
tests = ['fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_pass',
'fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_fail',
'fuel_plugin.tests.functional.dummy_tests.general_test.Dummy_test.test_fast_pass']
cluster_id = 999
self.client.start_testrun(testset, cluster_id)
time.sleep(2)
r = self.client.restart_tests_last(testset, tests, cluster_id)
msg = 'Response was not empty after trying to restart running testset:\n {0}'.format(r.request)
self.assertTrue(r.is_empty, msg)

View File

@ -0,0 +1,13 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from requests import get
import sys
def main():
try:
r = get('http://localhost:8000/api/clusters').json()
except IOError or ValueError as e:
print e.message
return 1
cluster_id = next(item['id'] for item in r)
print cluster_id
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,67 @@
#!/bin/bash
function killapp {
netstat -nplt | grep 8989 | grep -o [0-9]*/python | grep -o [0-9]* &> /dev/null || { echo "Not running" && return 1; }
while netstat -nplt | grep 8989 &> /dev/null; do
declare app_pid=$(netstat -nplt | grep 8989 | grep -o [0-9]*/python | grep -o [0-9]*)
echo "Ostf-adapter pid is: $app_pid"
kill -9 $app_pid; done
}
function stopapp {
if netstat -nplt | grep 8989 &> /dev/null; then
supervisorctl stop ostf && killapp
else
echo "OSTF-server is not running"
fi
}
function startapp {
if netstat -nplt | grep 8989 | grep -o [0-9]*/python | grep -o [0-9]* &> /dev/null; then
echo "Server is already running" && return 1; fi
supervisorctl start ostf
touch testing.log
sleep 5
count=1
while ! tail -1 testing.log | grep "serving on" &> /dev/null || ((count != 20)) ; do sleep 3; ((count+=1)) ; done
nc -xvw 2 0.0.0.0 8989 &> /dev/null && echo "Working like a charm" || echo "Not working"
}
function update_tests {
if [[ -z $1 && -z $OSTF_TESTS_BRANCH ]] ; then echo "Please specify a branch"; return 1; fi
if [[ ! -z $1 ]] ; then
git ls-remote --heads git@github.com:Mirantis/fuel-ostf-tests.git $1 | grep $1 &> /dev/null || { echo "No branch" && return 1; }
export OSTF_TESTS_BRANCH=$1
fi
! pip freeze | grep ostf-tests &> /dev/null || pip uninstall -y ostf-tests
pip install -e git+ssh://git@github.com/Mirantis/fuel-ostf-tests.git@"$OSTF_TESTS_BRANCH"#egg=ostf-tests
}
function update_adapter {
if [[ -z $1 && -z $OSTF_ADAPTER_BRANCH ]] ; then echo "Please specify a branch"; return 1; fi
if [[ ! -z $1 ]] ; then
git ls-remote --heads git@github.com:Mirantis/fuel-ostf-tests.git $1 | grep $1 &> /dev/null || { echo "No branch" && return 1; }
export OSTF_ADAPTER_BRANCH=$1
fi
! pip freeze | grep testing_adapter &> /dev/null || pip uninstall -y testing_adapter
pip install -e git+ssh://git@github.com/Mirantis/fuel-ostf-tests.git@"$OSTF_ADAPTER_BRANCH"#egg=ostf-plugin
}
function migrate_db {
service postgresql restart
while ! service postgresql status | grep running &> /dev/null; do sleep 1; done
sleep 30
export PGPASSWORD='ostf'
psql -U postgres -h localhost -c "drop database if exists testing_adapter"
psql -U postgres -h localhost -c "drop user adapter"
psql -U postgres -h localhost -c "create role adapter with nosuperuser createdb password 'demo' login"
psql -U postgres -h localhost -c "create database testing_adapter"
ostf-server --after-initialization-environment-hook --dbpath=postgresql+psycopg2://adapter:demo@localhost/testing_adapter
}
function run_functional_tests {
[[ ! -z $WORKSPACE ]] || export WORKSPACE=$(pwd)
nosetests -q fuel_plugin/functional/tests.py:AdapterTests --with-xunit --xunit-file=$WORKSPACE/reports/functional.xml
}

View File

@ -0,0 +1,5 @@
export PGPASSWORD='ostf'
psql -U postgres -h localhost -c "drop user adapter"
psql -U postgres -h localhost -c "create role adapter with nosuperuser createdb password 'demo' login"
psql -U postgres -h localhost -c "drop database if exists testing_adapter"
psql -U postgres -h localhost -c "create database testing_adapter"

View File

@ -0,0 +1,236 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Profiled execution.
profile=no
# Add <file or directory> to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=CVS
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
[MESSAGES CONTROL]
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifier separated by comma (,) or put this option
# multiple time.
disable=F0401,R0201,W0311,C0111
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html
output-format=parseable
# Include message's id in output
include-ids=yes
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (R0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Add a comment according to your evaluation note. This is used by the global
# evaluation report (R0004).
comment=no
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=120
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching names used for dummy variables (i.e. not used).
dummy-variables-rgx=_|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed.
generated-members=REQUEST,acl_users,aq_parent
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[BASIC]
# Required attributes for module, separated by a comma
required-attributes=
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter,apply,input
# Regular expression which should only match correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression which should only match correct module level names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression which should only match correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Regular expression which should only match correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct list comprehension /
# generator expression variable names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Good variable names which should always be accepted, separated by a comma
good-names=app,uwsgi,e,i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do
# not require a docstring
no-docstring-rgx=__.*__|[Tt]est.*
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=20
# Maximum number of return / yield for function / method body
max-returns=10
# Maximum number of branch for function / method body
max-branchs=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=10
# Minimum number of public methods for a class (see R0903).
min-public-methods=0
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp

View File

@ -0,0 +1,37 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 json import loads, dumps
def main():
with open('fuel_plugin/ostf_adapter/commands.json', 'rw+') as commands:
data = loads(commands.read())
for item in data:
if 'argv' in data[item] and item in ['fuel_sanity', 'fuel_smoke']:
if "--with-xunit" not in data[item]['argv']:
data[item]['argv'].extend(["--with-xunit", '--xunit-file={0}.xml'.format(item)])
elif item in ['fuel_sanity', 'fuel_smoke']:
data[item]['argv'] = ["--with-xunit", ]
test_apps = {"plugin_general": {"test_path": "fuel_plugin/tests/functional/dummy_tests/general_test.py", "driver": "nose"},
"plugin_stopped": {"test_path": "fuel_plugin/tests/functional/dummy_tests/stopped_test.py", "driver": "nose"}}
if 'plugin_general' not in data or 'plugin_stopped' not in data:
data.update(test_apps)
commands.seek(0)
commands.write(dumps(data))
commands.truncate()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,13 @@
# Copyright 2013 Mirantis, Inc.
#
# 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.

View File

@ -0,0 +1,49 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 mock import patch
import unittest2
from fuel_plugin.ostf_adapter.nose_plugin import nose_discovery
from fuel_plugin.ostf_adapter.storage import models
stopped__profile__ = {
"id": "stopped_test",
"driver": "nose",
"test_path": "functional/dummy_tests/stopped_test.py",
"description": "Long running 25 secs fake tests"
}
general__profile__ = {
"id": "general_test",
"driver": "nose",
"test_path": "functional/dummy_tests/general_test.py",
"description": "General fake tests"
}
@patch('fuel_plugin.ostf_adapter.nose_plugin.nose_discovery.engine')
class TestNoseDiscovery(unittest2.TestCase):
def setUp(self):
self.fixtures = [models.TestSet(**stopped__profile__),
models.TestSet(**general__profile__)]
self.fixtures_iter = iter(self.fixtures)
def test_discovery(self, engine):
engine.get_session().merge.return_value = \
lambda *args, **kwargs: self.fixtures_iter.next()
nose_discovery.discovery(path='functional/dummy_tests')
self.assertEqual(engine.get_session().merge.call_count, 2)

View File

@ -0,0 +1,125 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 json
from mock import patch, MagicMock
import unittest2
from fuel_plugin.ostf_adapter.wsgi import controllers
from fuel_plugin.ostf_adapter.storage import models
@patch('fuel_plugin.ostf_adapter.wsgi.controllers.request')
class TestTestsController(unittest2.TestCase):
def setUp(self):
self.fixtures = [models.Test(), models.Test()]
self.controller = controllers.TestsController()
def test_get_all(self, request):
request.session.query().all.return_value = self.fixtures
res = self.controller.get_all()
self.assertEqual(res, [f.frontend for f in self.fixtures])
@patch('fuel_plugin.ostf_adapter.wsgi.controllers.request')
class TestTestSetsController(unittest2.TestCase):
def setUp(self):
self.fixtures = [models.TestSet(), models.TestSet()]
self.controller = controllers.TestsetsController()
def test_get_all(self, request):
request.session.query().all.return_value = self.fixtures
res = self.controller.get_all()
self.assertEqual(res, [f.frontend for f in self.fixtures])
@patch('fuel_plugin.ostf_adapter.wsgi.controllers.request')
class TestTestRunsController(unittest2.TestCase):
def setUp(self):
self.fixtures = [models.TestRun(status='finished'),
models.TestRun(status='running')]
self.fixtures[0].test_set = models.TestSet(driver='nose')
self.storage = MagicMock()
self.plugin = MagicMock()
self.session = MagicMock()
self.controller = controllers.TestrunsController()
def test_get_all(self, request):
request.session.query().all.return_value = self.fixtures
res = self.controller.get_all()
self.assertEqual(res, [f.frontend for f in self.fixtures])
def test_get_one(self, request):
request.session.query().filter_by().first.return_value = \
self.fixtures[0]
res = self.controller.get_one(1)
self.assertEqual(res, self.fixtures[0].frontend)
@patch('fuel_plugin.ostf_adapter.wsgi.controllers.models')
def test_post(self, models, request):
request.storage = self.storage
testruns = [
{'testset': 'test_simple',
'metadata': {'cluster_id': 3}
},
{'testset': 'test_simple',
'metadata': {'cluster_id': 4}
}]
request.body = json.dumps(testruns)
fixtures_iterable = (f.frontend for f in self.fixtures)
models.TestRun.start.side_effect = \
lambda *args, **kwargs: fixtures_iterable.next()
res = self.controller.post()
self.assertEqual(res, [f.frontend for f in self.fixtures])
@patch('fuel_plugin.ostf_adapter.wsgi.controllers.models')
def test_put_stopped(self, models, request):
request.storage = self.storage
testruns = [
{'id': 1,
'metadata': {'cluster_id': 4},
'status': 'stopped'
}]
request.body = json.dumps(testruns)
models.TestRun.get_test_run().stop.side_effect = \
lambda *args, **kwargs: self.fixtures[0].frontend
res = self.controller.put()
self.assertEqual(res, [self.fixtures[0].frontend])
@patch('fuel_plugin.ostf_adapter.wsgi.controllers.models')
def test_put_restarted(self, models, request):
request.storage = self.storage
testruns = [
{'id': 1,
'metadata': {'cluster_id': 4},
'status': 'restarted'
}]
request.body = json.dumps(testruns)
models.TestRun.get_test_run().restart.side_effect = \
lambda *args, **kwargs: self.fixtures[0].frontend
res = self.controller.put()
self.assertEqual(res, [self.fixtures[0].frontend])
def test_get_last(self, request):
cluster_id = 1
request.session.query().group_by().filter_by.return_value = [10, 11]
request.session.query().options().filter.return_value = self.fixtures
res = self.controller.get_last(cluster_id)
self.assertEqual(res, [f.frontend for f in self.fixtures])

View File

@ -0,0 +1,78 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 unittest2
from mock import patch, MagicMock
import json
from webtest import TestApp
from fuel_plugin.ostf_adapter.wsgi import app
@patch('fuel_plugin.ostf_adapter.wsgi.controllers.request')
class WsgiInterfaceTests(unittest2.TestCase):
def setUp(self):
self.app = TestApp(app.setup_app())
def test_get_all_tests(self, request):
self.app.get('/v1/tests')
def test_get_one_test(self, request):
self.assertRaises(NotImplementedError,
self.app.get,
'/v1/tests/1')
def test_get_all_testsets(self, request):
self.app.get('/v1/testsets')
def test_get_one_testset(self, request):
self.app.get('/v1/testsets/plugin_test')
def test_get_one_testruns(self, request):
self.app.get('/v1/testruns/1')
def test_get_all_testruns(self, request):
self.app.get('/v1/testruns')
@patch('fuel_plugin.ostf_adapter.wsgi.controllers.models')
def test_post_testruns(self, models, request):
testruns = [
{'testset': 'test_simple',
'metadata': {'cluster_id': 3}
},
{'testset': 'test_simple',
'metadata': {'cluster_id': 4}
}]
request.body = json.dumps(testruns)
models.TestRun.start.return_value = {}
self.app.post_json('/v1/testruns', testruns)
def test_put_testruns(self, request):
testruns = [
{'id': 2,
'metadata': {'cluster_id': 3},
'status': 'non_exist'
},
{'id': 1,
'metadata': {'cluster_id': 4},
'status': 'non_exist'
}]
request.body = json.dumps(testruns)
request.storage.get_test_run.return_value = MagicMock(frontend={})
self.app.put_json('/v1/testruns', testruns)
def test_get_last_testruns(self, request):
self.app.get('/v1/testruns/last/101')

View File

@ -1,3 +1,18 @@
# Copyright 2013 Mirantis, Inc.
#
# 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 multiprocessing
import setuptools
@ -17,7 +32,6 @@ requirements = [
'nose==1.3.0',
'oslo.config==1.1.1',
'paramiko==1.10.1',
'pbr>=0.5.21,<1.0',
'prettytable==0.7.2',
'pyOpenSSL==0.13',
'pycrypto==2.6',
@ -35,23 +49,78 @@ requirements = [
'testresources==0.2.7',
'warlock==1.0.1',
'wsgiref==0.1.2',
'pyyaml==3.10'
'pyyaml==3.10',
'Mako==0.8.1',
'MarkupSafe==0.18',
'SQLAlchemy==0.8.2',
'WebOb==1.2.3',
'WebTest==2.0.6',
'alembic==0.5.0',
'beautifulsoup4==4.2.1',
'gevent==0.13.8',
'greenlet==0.4.1',
'pecan==0.3.0',
'psycogreen==1.0',
'psycopg2==2.5.1',
'simplegeneric==0.8.1',
'stevedore==0.10',
'waitress==0.8.5',
'WSME==0.5b2'
]
test_requires = [
'mock==1.0.1',
'pep8==1.4.6',
'py==1.4.15',
'six==1.3.0',
'tox==1.5.0',
'unittest2',
'nose',
'requests'
]
setuptools.setup(
name='ostf_tests',
name='fuel_ostf',
version='0.1',
description='cloud computing testing',
zip_safe=False,
test_suite='fuel_plugin/tests',
classifiers=[
'Development Status :: 3 - Alpha',
'Framework :: Setuptools Plugin',
'Environment :: OpenStack',
'Intended Audience :: Information Technology',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Topic :: System :: Testing',
],
packages=setuptools.find_packages(),
include_package_data=True,
install_requires=requirements,
entry_points={
'plugins': [
('nose = fuel_plugin.ostf_adapter.'
'nose_plugin.nose_adapter:NoseDriver')
],
'console_scripts': [
'ostf-server = fuel_plugin.bin.adapter_api:main',
('update-commands = fuel_plugin.tests.'
'test_utils.update_commands:main')
]
},
)

9
tools/test-requires Normal file
View File

@ -0,0 +1,9 @@
WebTest==2.0.6
mock==1.0.1
pep8==1.4.6
py==1.4.15
six==1.3.0
tox==1.5.0
unittest2==0.5.1
coverage==3.6
requests==1.2.3

16
tox.ini
View File

@ -4,8 +4,20 @@
# and then run "tox" from this directory.
[tox]
envlist = pep8
envlist = py27, pep8, cover
[testenv]
commands = nosetests fuel_plugin.tests.unit
deps = -r{toxinidir}/tools/test-requires
[testenv:pep8]
deps = pep8
commands = pep8 --repeat --show-source --exclude=.venv,.tox,dist,doc,*egg .
commands = pep8 --repeat --show-source --exclude=.venv,.tox,dist,./build,doc,*egg,tests,functional,test_utils,ostf_client .
[testenv:cover]
commands = nosetests fuel_plugin.tests.unit --no-path-adjustment --with-coverage --cover-erase --cover-package=fuel_plugin.ostf_adapter
#[testenv:funct]
#deps = requests
#commands = nosetests functional/tests.py:AdapterTests