Init REST API

This patch adds REST API for next operations:
 - getting of list plugins (GET /v1/plugins),
 - getting of suites from plugin (GET /v1/plugins/<name>/suites)
 - getting of tests from plugin (GET /v1/plugins/<name>/suites/tests)
 - getting of tests from suites
   (GET /v1/plugins/<name>/suites/<suite>/tests)
 - executing of suites in plugin (POST /v1/plugins/<name>/suites)
 - executing of suite in plugin (POST /v1/plugins/<name>/suites/<suite>)
 - executing of test (POST /v1/plugins/<name>/suites/tests/<test>)

Change-Id: I8a5c7a14a791d62fc856526dbd5585430ec2d555
This commit is contained in:
Svetlana Shturm 2015-03-11 11:04:00 +00:00
parent cd3e966d0a
commit 8e32dc6c0f
11 changed files with 466 additions and 6 deletions

View File

@ -123,3 +123,49 @@ Links
* OSTF contributor's guide - http://docs.mirantis.com/fuel-dev/develop/ostf_contributors_guide.html)
* OSTF source code - https://github.com/stackforge/fuel-ostf
========
REST API
========
Run server
----------
.. code-block:: bash
$ cloudvalidation-server --config-file=path_to_config
* Running on http://127.0.0.1:8777/ (Press CTRL+C to quit)
Example of config
-----------------
[rest]
server_host=127.0.0.1
server_port=8777
log_file=/var/log/ostf.log
debug=False
List of supported operations
----------------------------
- get list of supported plugins
GET /v1/plugins?load_tests=True/False
In load_tests=True case tests for plugin will be shown.
- get suites in plugin
GET /v1/plugins/<plugin_name>/suites
- get tests for all suites in plugin
GET /v1/plugins/<plugin_name>/suites/tests
- get tests per suite in plugin
GET /v1/plugins/<plugin_name>/suites/<suite>/tests
- run suites for plugin
POST /v1/plugins/<plugin_name>/suites
- run suite for plugin
POST /v1/plugins/<plugin_name>/suites/<suite>
- run test for plugin
/v1/plugins/<plugin_name>/suites/tests/<test>

View File

@ -30,6 +30,7 @@ sanity_group = cfg.OptGroup("sanity", "Sanity configuration group.")
smoke_group = cfg.OptGroup("smoke", "Smoke configuration group.")
platform_group = cfg.OptGroup("platform",
"Platform functional configuration group.")
rest_group = cfg.OptGroup("rest", "Cloudvalidation ReST API service options.")
ha_group = cfg.OptGroup("high_availability", "HA configuration group.")
@ -61,6 +62,22 @@ ha_opts = [
cfg.MultiStrOpt("enabled_tests", default=[]),
]
rest_opts = [
cfg.StrOpt('server_host',
default='127.0.0.1',
help="adapter host"),
cfg.IntOpt('server_port',
default=8777,
help="Port number"),
cfg.StrOpt('log_file',
default='/var/log/ostf.log',
help=""),
cfg.StrOpt('debug',
default=False,
help="Debug for REST API."),
]
CONF = cfg.CONF
CONF.register_opts(common_opts)
@ -68,11 +85,13 @@ CONF.register_group(sanity_group)
CONF.register_group(smoke_group)
CONF.register_group(platform_group)
CONF.register_group(ha_group)
CONF.register_group(rest_group)
CONF.register_opts(sanity_opts, sanity_group)
CONF.register_opts(smoke_opts, smoke_group)
CONF.register_opts(platform_opts, platform_group)
CONF.register_opts(ha_opts, ha_group)
CONF.register_opts(rest_opts, rest_group)
def parse_args(argv, default_config_files=None):

View File

@ -0,0 +1,53 @@
# Copyright 2015 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 signal
import sys
import flask
from flask.ext import restful
from oslo_config import cfg
from cloudv_ostf_adapter.common import cfg as config
from cloudv_ostf_adapter import wsgi
CONF = cfg.CONF
app = flask.Flask('cloudv_ostf_adapter')
api = restful.Api(app)
api.add_resource(wsgi.Plugins, '/v1/plugins')
api.add_resource(wsgi.PluginSuite,
'/v1/plugins/<plugin>/suites')
api.add_resource(wsgi.PluginTests,
'/v1/plugins/<plugin>/suites/tests')
api.add_resource(wsgi.Suites,
'/v1/plugins/<plugin>/suites/<suite>',
'/v1/plugins/<plugin>/suites/<suite>/tests')
api.add_resource(wsgi.Tests,
'/v1/plugins/<plugin>/suites/tests/<test>')
def main():
config.parse_args(sys.argv)
host, port = CONF.rest.server_host, CONF.rest.server_port
try:
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
signal.signal(signal.SIGHUP, signal.SIG_IGN)
app.run(host=host, port=port, debug=CONF.rest.debug)
except KeyboardInterrupt:
pass

View File

@ -23,9 +23,29 @@ CONF = cfg.CONF
SUITES = [fake_plugin_tests]
class FakeTest(object):
def __init__(self):
self.description = {
"test": 'fake_test',
"report": "",
"result": "passed",
"duration": "0.1"
}
class FakeValidationPlugin(base.ValidationPlugin):
def __init__(self, load_tests=True):
name = 'fake'
self.test = FakeTest()
super(FakeValidationPlugin, self).__init__(
name, SUITES, load_tests=load_tests)
def run_suites(self):
return [self.test]
def run_suite(self, suite):
return [self.test]
def run_test(self, test):
return [self.test]

View File

@ -0,0 +1,176 @@
# Copyright 2015 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 testtools
from cloudv_ostf_adapter import server
from cloudv_ostf_adapter.tests.unittests.fakes.fake_plugin import health_plugin
from cloudv_ostf_adapter import wsgi
class TestServer(testtools.TestCase):
def setUp(self):
self.plugin = health_plugin.FakeValidationPlugin()
server.app.config['TESTING'] = True
self.app = server.app.test_client()
self.actual_plugins = wsgi.validation_plugin.VALIDATION_PLUGINS
wsgi.validation_plugin.VALIDATION_PLUGINS = [self.plugin.__class__]
super(TestServer, self).setUp()
def tearDown(self):
wsgi.validation_plugin.VALIDATION_PLUGINS = self.actual_plugins
super(TestServer, self).tearDown()
def test_urlmap(self):
links = []
check_list = [
'/v1/plugins',
'/v1/plugins/<plugin>/suites/tests/<test>',
'/v1/plugins/<plugin>/suites/<suite>/tests',
'/v1/plugins/<plugin>/suites/tests',
'/v1/plugins/<plugin>/suites/<suite>',
'/v1/plugins/<plugin>/suites'
]
for rule in server.app.url_map.iter_rules():
links.append(str(rule))
self.assertEqual(set(check_list) & set(links), set(check_list))
def _resp_to_dict(self, data):
if type(data) == bytes:
data = data.decode('utf-8')
return json.loads(data)
def test_plugins_no_load_tests(self):
rv = self.app.get('/v1/plugins').data
check = {
'plugins': [{'name': self.plugin.name,
'suites': self.plugin.suites,
'tests': []}]
}
self.assertEqual(self._resp_to_dict(rv), check)
def test_plugins_load_tests(self):
rv = self.app.get('/v1/plugins?load_tests=True').data
check = {
'plugins': [{'name': self.plugin.name,
'suites': self.plugin.suites,
'tests': self.plugin.tests}]
}
self.assertEqual(self._resp_to_dict(rv), check)
def test_plugin_not_found(self):
rv = self.app.get('/v1/plugins/fake2/suites').data
check = {"message": "Unsupported plugin fake2."}
self.assertEqual(self._resp_to_dict(rv), check)
def test_plugin_suites(self):
rv = self.app.get('/v1/plugins/fake/suites').data
check = {"plugin": {"name": self.plugin.name,
"suites": self.plugin.suites}}
self.assertEqual(self._resp_to_dict(rv), check)
def test_suite_plugin_not_found(self):
rv = self.app.get(
'/v1/plugins/fake2/suites/fake/tests').data
check = {u'message': u'Unsupported plugin fake2.'}
self.assertEqual(self._resp_to_dict(rv), check)
def test_suite_tests(self):
suite = self.plugin.suites[0]
url = '/v1/plugins/fake/suites/%s/tests' % suite
rv = self.app.get(url).data
tests = self.plugin.get_tests_by_suite(suite)
check = {
"plugin": {"name": self.plugin.name,
"suite": {"name": suite,
"tests": tests}}}
self.assertEqual(self._resp_to_dict(rv), check)
def test_suite_not_found(self):
rv = self.app.get(
'/v1/plugins/fake/suites/fake/tests').data
check = {u'message': u'Unknown suite fake.'}
self.assertEqual(self._resp_to_dict(rv), check)
def test_plugin_tests_not_found(self):
rv = self.app.get(
'/v1/plugins/fake2/suites/tests').data
check = {u'message': u'Unsupported plugin fake2.'}
self.assertEqual(self._resp_to_dict(rv), check)
def test_plugin_tests(self):
rv = self.app.get(
'/v1/plugins/fake/suites/tests').data
check = {"plugin": {"name": self.plugin.name,
"tests": self.plugin.tests}}
self.assertEqual(self._resp_to_dict(rv), check)
def test_run_suites(self):
rv = self.app.post(
'/v1/plugins/fake/suites').data
check = {
u'plugin': {u'name': self.plugin.name,
u'report': [self.plugin.test.description]}}
self.assertEqual(self._resp_to_dict(rv), check)
def test_run_suites_plugin_not_found(self):
rv = self.app.post(
'/v1/plugins/fake2/suites').data
check = {u'message': u'Unsupported plugin fake2.'}
self.assertEqual(self._resp_to_dict(rv), check)
def test_run_suite(self):
suite = self.plugin.suites[0]
rv = self.app.post(
'/v1/plugins/fake/suites/%s' % suite).data
check = {
u'suite': {u'name': suite,
u'report': [self.plugin.test.description]}}
self.assertEqual(self._resp_to_dict(rv), check)
def test_run_suite_plugin_not_found(self):
rv = self.app.post(
'/v1/plugins/fake2/suites/fake_suite').data
check = {u'message': u'Unsupported plugin fake2.'}
self.assertEqual(self._resp_to_dict(rv), check)
def test_run_suite_suite_not_found(self):
rv = self.app.post(
'/v1/plugins/fake/suites/fake_suite').data
check = {u'message': u'Unknown suite fake_suite.'}
self.assertEqual(self._resp_to_dict(rv), check)
def test_run_test(self):
test = self.plugin.tests[0]
rv = self.app.post(
'/v1/plugins/fake/suites/tests/%s' % test).data
check = {
u'plugin': {u'name': self.plugin.name,
u'test': test,
u'report': [self.plugin.test.description]}}
self.assertEqual(self._resp_to_dict(rv), check)
def test_run_test_plugin_not_found(self):
rv = self.app.post(
'/v1/plugins/fake2/suites/tests/fake_test').data
check = {u'message': u'Unsupported plugin fake2.'}
self.assertEqual(self._resp_to_dict(rv), check)
def test_run_test_not_found(self):
rv = self.app.post(
'/v1/plugins/fake/suites/tests/fake_test').data
check = {u'message': u'Test fake_test not found.'}
self.assertEqual(self._resp_to_dict(rv), check)

View File

@ -60,10 +60,10 @@ class ValidationPlugin(object):
self.name = name
self.suites = __suites
self._suites = suites
self.tests = (self._get_tests()
self.tests = (self.get_tests()
if load_tests else [])
def _get_tests(self):
def get_tests(self):
"""
Test collector
"""
@ -94,7 +94,7 @@ class ValidationPlugin(object):
})
return test_suites_paths
def _get_tests_by_suite(self, suite):
def get_tests_by_suite(self, suite):
tests = []
for test in self.tests:
if suite in test:

View File

@ -79,9 +79,9 @@ class FuelHealthPlugin(base.ValidationPlugin):
super(FuelHealthPlugin, self).__init__(
'fuel_health', SUITES, load_tests=load_tests)
def _get_tests(self):
def get_tests(self):
try:
return super(FuelHealthPlugin, self)._get_tests()
return super(FuelHealthPlugin, self).get_tests()
except Exception:
print("fuel_health is not installed.")
@ -134,7 +134,7 @@ class FuelHealthPlugin(base.ValidationPlugin):
raise Exception(
"%s is a test case, but not test suite." % suite)
else:
tests = self._get_tests_by_suite(suite)
tests = self.get_tests_by_suite(suite)
test_suites_paths = self.setup_execution(tests)
reports = self._execute_and_report(test_suites_paths)
sys.stderr = safe_stderr

View File

@ -0,0 +1,137 @@
# Copyright 2015 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 flask.ext import restful
from flask.ext.restful import abort
from flask.ext.restful import reqparse
from oslo_config import cfg
from cloudv_ostf_adapter import validation_plugin
CONF = cfg.CONF
class BaseTests(restful.Resource):
def __init__(self, *args, **kwargs):
super(BaseTests, self).__init__(*args, **kwargs)
self.plugins = {}
for plugin in validation_plugin.VALIDATION_PLUGINS:
_plugin = plugin(load_tests=False)
self.plugins[_plugin.name] = _plugin
def load_tests(self):
for plugin in self.plugins.values():
plugin.tests = plugin.get_tests()
def get_plugin(self, **kwargs):
plugin = kwargs.pop('plugin', None)
if plugin is None or plugin not in self.plugins:
abort(404,
message='Unsupported plugin %s.' % plugin)
return self.plugins[plugin]
def get_suite(self, plugin, suite=None):
if suite not in plugin.suites:
abort(404,
message='Unknown suite %s.' % suite)
return suite
class Plugins(BaseTests):
def __init__(self, *args, **kwargs):
super(Plugins, self).__init__(*args, **kwargs)
self.parser = reqparse.RequestParser()
self.parser.add_argument('load_tests',
type=str,
location='args',
required=False)
def get(self, **kwargs):
args = self.parser.parse_args()
load_tests = args.pop('load_tests', False)
if load_tests in ['True', 'true', '1']:
self.load_tests()
plugins = [
{'name': p.name,
'suites': p.suites,
'tests': p.tests}
for p in self.plugins.values()
]
return {'plugins': plugins}
class PluginSuite(BaseTests):
def get(self, **kwargs):
plugin = self.get_plugin(**kwargs)
return {'plugin': {'name': plugin.name,
'suites': plugin.suites}}
def post(self, **kwargs):
plugin = self.get_plugin(**kwargs)
self.load_tests()
reports = plugin.run_suites()
report = [r.description for r in reports]
return {"plugin": {"name": plugin.name,
"report": report}}
class PluginTests(BaseTests):
def get(self, **kwargs):
plugin = self.get_plugin(**kwargs)
self.load_tests()
return {'plugin': {'name': plugin.name,
'tests': plugin.tests}}
class Suites(BaseTests):
def get(self, **kwargs):
plugin = self.get_plugin(**kwargs)
_suite = kwargs.pop('suite', None)
suite = self.get_suite(plugin, suite=_suite)
self.load_tests()
tests = plugin.get_tests_by_suite(suite)
return {'plugin': {'name': plugin.name,
'suite': {'name': suite,
'tests': tests}}}
def post(self, **kwargs):
plugin = self.get_plugin(**kwargs)
_suite = kwargs.pop('suite', None)
suite = self.get_suite(plugin, suite=_suite)
self.load_tests()
reports = plugin.run_suite(suite)
report = [r.description for r in reports]
return {"suite": {"name": suite,
"report": report}}
class Tests(BaseTests):
def post(self, **kwargs):
plugin = self.get_plugin(**kwargs)
self.load_tests()
test = kwargs.pop('test', None)
if test is None or test not in plugin.tests:
abort(404,
message="Test %s not found." % test)
reports = plugin.run_test(test)
report = [r.description for r in reports]
return {"plugin": {"name": plugin.name,
"test": test,
"report": report}}

View File

@ -1,6 +1,12 @@
[DEFAULT]
health_check_config_path = /home/dmakogon/Documents/MCV/repo/cloudv-ostf-adapter/etc/cloudv_ostf_adapter/test.conf
[rest]
server_host=127.0.0.1
server_port=8777
log_file=/var/log/ostf.log
debug=False
[sanity]
[smoke]

View File

@ -2,6 +2,8 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
flask
flask-restful
nose
oslo.config>=1.6.0 # Apache-2.0
pbr>=0.6,!=0.7,<1.0

View File

@ -26,6 +26,7 @@ domain = cloudv_ostf_adapter
[entry_points]
console_scripts =
cloudvalidation = cloudv_ostf_adapter.cmd.cloudv_runner:main
cloudvalidation-server = cloudv_ostf_adapter.server:main
[global]
setup-hooks =