From 64b99e5008e22072a841086ea1707c3bcee384d3 Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Thu, 19 Mar 2015 15:53:53 +0200 Subject: [PATCH] Introducing python client for cloudvalidation ReST service Change-Id: I01748eaa30eaf1d79b8178a36cebdfddd91bc949 --- README.rst | 18 +++ cloudv_ostf_adapter/cloudv_client/__init__.py | 0 cloudv_ostf_adapter/cloudv_client/client.py | 33 +++++ cloudv_ostf_adapter/cloudv_client/plugins.py | 38 +++++ cloudv_ostf_adapter/cloudv_client/suites.py | 68 +++++++++ cloudv_ostf_adapter/cloudv_client/tests.py | 41 ++++++ .../cmd/{cloudv_runner.py => cli.py} | 0 cloudv_ostf_adapter/cmd/client.py | 130 ++++++++++++++++++ cloudv_ostf_adapter/{ => cmd}/server.py | 9 +- cloudv_ostf_adapter/common/cfg.py | 10 +- cloudv_ostf_adapter/common/exception.py | 45 ++++++ .../tests/unittests/test_server.py | 4 +- cloudv_ostf_adapter/wsgi/__init__.py | 1 + requirements.txt | 3 + setup.cfg | 5 +- 15 files changed, 400 insertions(+), 5 deletions(-) create mode 100644 cloudv_ostf_adapter/cloudv_client/__init__.py create mode 100644 cloudv_ostf_adapter/cloudv_client/client.py create mode 100644 cloudv_ostf_adapter/cloudv_client/plugins.py create mode 100644 cloudv_ostf_adapter/cloudv_client/suites.py create mode 100644 cloudv_ostf_adapter/cloudv_client/tests.py rename cloudv_ostf_adapter/cmd/{cloudv_runner.py => cli.py} (100%) create mode 100644 cloudv_ostf_adapter/cmd/client.py rename cloudv_ostf_adapter/{ => cmd}/server.py (89%) create mode 100644 cloudv_ostf_adapter/common/exception.py diff --git a/README.rst b/README.rst index b54bbeb..1e055a0 100644 --- a/README.rst +++ b/README.rst @@ -23,6 +23,9 @@ Supported plugins:: Usage ----- + +Please note that if you're using Fuel OSTF plugin, you have to install it manually. + .. code-block:: bash $ cloudvalidation cloud-health-check {argument} [argument_parameters] @@ -169,3 +172,18 @@ List of supported operations - run test for plugin /v1/plugins//suites/tests/ + +===================== +REST API Client usage +===================== + +.. code-block:: python + + from cloudv_ostf_adapter.cloudv_client import client + + cloudvclient = client.Client(CONF.host, CONF.port, CONF.api_version) + + plugins = cloudvclient.plugins.list() + plugin_one = plugins[0]['name'] + + suites = cloudvalidation.suites.list_suites(plugin_one) diff --git a/cloudv_ostf_adapter/cloudv_client/__init__.py b/cloudv_ostf_adapter/cloudv_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudv_ostf_adapter/cloudv_client/client.py b/cloudv_ostf_adapter/cloudv_client/client.py new file mode 100644 index 0000000..086b57f --- /dev/null +++ b/cloudv_ostf_adapter/cloudv_client/client.py @@ -0,0 +1,33 @@ +# 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 cloudv_ostf_adapter.common import cfg + +from cloudv_ostf_adapter.cloudv_client import plugins +from cloudv_ostf_adapter.cloudv_client import suites +from cloudv_ostf_adapter.cloudv_client import tests + +CONF = cfg.CONF + + +class Client(object): + + def __init__(self, host, port, api_version): + kwargs = { + 'host': host, + 'port': port, + 'api_version': api_version + } + self.plugins = plugins.Plugins(**kwargs) + self.suites = suites.Suites(**kwargs) + self.tests = tests.Tests(**kwargs) diff --git a/cloudv_ostf_adapter/cloudv_client/plugins.py b/cloudv_ostf_adapter/cloudv_client/plugins.py new file mode 100644 index 0000000..7445473 --- /dev/null +++ b/cloudv_ostf_adapter/cloudv_client/plugins.py @@ -0,0 +1,38 @@ +# 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. + +try: + import simplejson as json +except ImportError: + import json + +import requests + +from cloudv_ostf_adapter.common import exception + + +class Plugins(object): + + route = "http://%(host)s:%(port)d/%(api_version)s/plugins" + + def __init__(self, **kwargs): + self.url = self.route % kwargs + + def list(self, load_tests=True): + params = {'load_tests': load_tests} + response = requests.get(self.url, params=params) + if not response.ok: + raise exception.exception_mapping.get(response.status_code)() + resp = json.loads(response.content) + return resp['plugins'] diff --git a/cloudv_ostf_adapter/cloudv_client/suites.py b/cloudv_ostf_adapter/cloudv_client/suites.py new file mode 100644 index 0000000..7a3ba4a --- /dev/null +++ b/cloudv_ostf_adapter/cloudv_client/suites.py @@ -0,0 +1,68 @@ +# 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. + +try: + import simplejson as json +except ImportError: + import json + +import requests + +from cloudv_ostf_adapter.common import exception + + +class Suites(object): + + _suite_route = ("http://%(host)s:%(port)d/%(api_version)s" + "/plugins/%(plugin)s/suites") + _suite_test_route = ("http://%(host)s:%(port)d/%(api_version)s/" + "plugins/%(plugin)s/suites/%(suite)s/tests") + _test_route = ("http://%(host)s:%(port)d/%(api_version)s/" + "plugins/%(plugin)s/suites/tests") + + def __init__(self, **kwargs): + self.kwargs = kwargs + + def list_suites(self, plugin): + self.kwargs.update({"plugin": plugin}) + suite_url = self._suite_route % self.kwargs + response = requests.get(suite_url) + if not response.ok: + raise exception.exception_mapping.get(response.status_code)() + return json.loads(response.content)['plugin'] + + def list_tests_for_suites(self, plugin): + self.kwargs.update({"plugin": plugin}) + suite_url = self._test_route % self.kwargs + response = requests.get(suite_url) + if not response.ok: + raise exception.exception_mapping.get(response.status_code)() + return json.loads(response.content)['plugin'] + + def run_suites(self, plugin): + self.kwargs.update({"plugin": plugin}) + suite_url = self._suite_route % self.kwargs + response = requests.post(suite_url, {}) + if not response.ok: + raise exception.exception_mapping.get(response.status_code)() + return json.loads(response.content)['plugin']['report'] + + def run_suite_tests(self, suite, plugin): + self.kwargs.update({"suite": suite}) + self.kwargs.update({"plugin": plugin}) + url = self._suite_test_route % self.kwargs + response = requests.post(url, {}) + if not response.ok: + raise exception.exception_mapping.get(response.status_code)() + return json.loads(response.content)['suite'] diff --git a/cloudv_ostf_adapter/cloudv_client/tests.py b/cloudv_ostf_adapter/cloudv_client/tests.py new file mode 100644 index 0000000..ebd4259 --- /dev/null +++ b/cloudv_ostf_adapter/cloudv_client/tests.py @@ -0,0 +1,41 @@ +# 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. + +try: + import simplejson as json +except ImportError: + import json + +import requests + +from cloudv_ostf_adapter.common import exception + + +class Tests(object): + + route = ("http://%(host)s:%(port)d/%(api_version)s" + "/plugins/%(plugin)s/suites/tests/%(test)s") + + def __init__(self, **kwargs): + self.kwargs = kwargs + + def run(self, test, plugin): + self.kwargs.update({"test": test}) + self.kwargs.update({"plugin": plugin}) + url = self.route % self.kwargs + response = requests.post(url, {}) + if not response.ok: + raise exception.exception_mapping.get( + response.status_code)() + return json.loads(response.content)['plugin']['report'] diff --git a/cloudv_ostf_adapter/cmd/cloudv_runner.py b/cloudv_ostf_adapter/cmd/cli.py similarity index 100% rename from cloudv_ostf_adapter/cmd/cloudv_runner.py rename to cloudv_ostf_adapter/cmd/cli.py diff --git a/cloudv_ostf_adapter/cmd/client.py b/cloudv_ostf_adapter/cmd/client.py new file mode 100644 index 0000000..a600c56 --- /dev/null +++ b/cloudv_ostf_adapter/cmd/client.py @@ -0,0 +1,130 @@ +# 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 sys + +from oslo_config import cfg + +from cloudv_ostf_adapter.common import cfg as config +from cloudv_ostf_adapter.cloudv_client import client +from cloudv_ostf_adapter.common import utils +from cloudv_ostf_adapter.cmd import _common as cmd + +CONF = cfg.CONF + + +class ClientV1Shell(object): + """ + Represents set of capabilities to interact with Cloudvalidation API + """ + + _client = client.Client(CONF.host, CONF.port, CONF.api_version) + + def list_plugins(self): + """ + List plugins + """ + resp = self._client.plugins.list(load_tests=False) + for plugin in resp: + suites = plugin['suites'] + plugin['suites'] = "\n".join(suites) + del plugin['tests'] + utils.print_dict(plugin) + + @cmd.args("--validation-plugin-name", dest="validation_plugin_name") + def list_plugin_suites(self, validation_plugin_name): + """ + List plugin suites + Required options: + --validation-plugin + """ + resp = self._client.suites.list_suites(validation_plugin_name) + suites = resp['suites'] + resp['suites'] = "\n".join(suites) + del resp['name'] + utils.print_dict(resp) + + @cmd.args("--validation-plugin-name", dest="validation_plugin_name") + def list_plugin_tests(self, validation_plugin_name): + """ + List plugin tests + Required options: + --validation-plugin + """ + resp = self._client.suites.list_tests_for_suites( + validation_plugin_name) + tests = resp['tests'] + resp['tests'] = "\n".join(tests) + del resp['name'] + utils.print_dict(resp) + + @cmd.args("--validation-plugin-name", dest="validation_plugin_name") + def run_suites(self, validation_plugin_name): + """ + Run plugin suites + Required options: + --validation-plugin + """ + resp = self._client.suites.run_suites(validation_plugin_name) + utils.print_list(resp, + ['test', 'duration', 'result', 'report'], + obj_is_dict=True) + + @cmd.args("--suite", dest="suite") + @cmd.args("--validation-plugin-name", dest="validation_plugin_name") + def run_suite(self, validation_plugin_name, suite): + """ + Run plugin suite + Required options: + --validation-plugin + --suite + """ + resp = self._client.suites.run_suite_tests( + suite, validation_plugin_name) + suite_test_reports = resp['report'] + utils.print_list(suite_test_reports, + ['test', 'duration', 'result', 'report'], + obj_is_dict=True) + + @cmd.args("--validation-plugin-name", dest="validation_plugin_name") + @cmd.args("--test", dest="test") + def run_test(self, validation_plugin_name, test): + """ + Run plugin test + Required options: + --validation-plugin + --test + """ + resp = self._client.tests.run(test, validation_plugin_name) + utils.print_list(resp, + ['test', 'duration', 'result', 'report'], + obj_is_dict=True) + + +CATS = { + 'cloud-health-check': ClientV1Shell +} + +category_opt = cfg.SubCommandOpt('category', + title='Command categories', + help='Available categories', + handler=cmd.add_command_parsers(CATS)) + + +def main(): + """Parse options and call the appropriate class/method.""" + cmd._main(CONF, config, category_opt, sys.argv) + +if __name__ == "__main__": + main() diff --git a/cloudv_ostf_adapter/server.py b/cloudv_ostf_adapter/cmd/server.py similarity index 89% rename from cloudv_ostf_adapter/server.py rename to cloudv_ostf_adapter/cmd/server.py index f374fd3..76aee64 100644 --- a/cloudv_ostf_adapter/server.py +++ b/cloudv_ostf_adapter/cmd/server.py @@ -16,6 +16,7 @@ import signal import sys import flask + from flask.ext import restful from oslo_config import cfg @@ -48,6 +49,12 @@ def main(): 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) + app.run(host=host, port=port, + debug=CONF.rest.debug, + use_reloader=True, + processes=100) except KeyboardInterrupt: pass + +if __name__ == "__main__": + main() diff --git a/cloudv_ostf_adapter/common/cfg.py b/cloudv_ostf_adapter/common/cfg.py index 09ab431..7aa1a6e 100644 --- a/cloudv_ostf_adapter/common/cfg.py +++ b/cloudv_ostf_adapter/common/cfg.py @@ -12,10 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +import os from oslo_config import cfg - from cloudv_ostf_adapter import version @@ -77,6 +77,11 @@ rest_opts = [ help="Debug for REST API."), ] +rest_client_opts = [ + cfg.StrOpt("host", default=os.environ.get("MCLOUDV_HOST", "localhost")), + cfg.IntOpt("port", default=os.environ.get("MCLOUDV_PORT", 8777)), + cfg.StrOpt("api_version", default="v1") +] CONF = cfg.CONF CONF.register_opts(common_opts) @@ -93,6 +98,9 @@ CONF.register_opts(platform_opts, platform_group) CONF.register_opts(ha_opts, ha_group) CONF.register_opts(rest_opts, rest_group) +#client opts +CONF.register_opts(rest_client_opts) + def parse_args(argv, default_config_files=None): cfg.CONF(args=argv[1:], diff --git a/cloudv_ostf_adapter/common/exception.py b/cloudv_ostf_adapter/common/exception.py new file mode 100644 index 0000000..9f4bea8 --- /dev/null +++ b/cloudv_ostf_adapter/common/exception.py @@ -0,0 +1,45 @@ +# 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. + + +class BaseHTTPException(Exception): + + def __init__(self, message=None, + http_code=400, **kwargs): + self.message = (message % kwargs + if message else self.message) + self.http_code = http_code + self.reason = "HTTP Code: %d." % http_code + super(BaseHTTPException, self).__init__(self.message + self.reason) + + +class BadRequest(BaseHTTPException): + http_code = 400 + message = "Bad request. " + + +class NotFound(BaseHTTPException): + http_code = 404 + message = "Not Found. " + + +class ConnectionRefused(BaseHTTPException): + http_code = 111 + message = "Server shutdowned. " + +exception_mapping = { + 111: ConnectionRefused, + 404: NotFound, + 400: BadRequest +} diff --git a/cloudv_ostf_adapter/tests/unittests/test_server.py b/cloudv_ostf_adapter/tests/unittests/test_server.py index a2406ba..35c201c 100644 --- a/cloudv_ostf_adapter/tests/unittests/test_server.py +++ b/cloudv_ostf_adapter/tests/unittests/test_server.py @@ -11,11 +11,12 @@ # 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.cmd import server from cloudv_ostf_adapter.tests.unittests.fakes.fake_plugin import health_plugin from cloudv_ostf_adapter import wsgi @@ -157,6 +158,7 @@ class TestServer(testtools.TestCase): test = self.plugin.tests[0] rv = self.app.post( '/v1/plugins/fake/suites/tests/%s' % test).data + self.plugin.test.description['test'] = test check = { u'plugin': {u'name': self.plugin.name, u'test': test, diff --git a/cloudv_ostf_adapter/wsgi/__init__.py b/cloudv_ostf_adapter/wsgi/__init__.py index 603a979..e00a0dd 100644 --- a/cloudv_ostf_adapter/wsgi/__init__.py +++ b/cloudv_ostf_adapter/wsgi/__init__.py @@ -132,6 +132,7 @@ class Tests(BaseTests): message="Test %s not found." % test) reports = plugin.run_test(test) report = [r.description for r in reports] + report[0]['test'] = test return {"plugin": {"name": plugin.name, "test": test, "report": report}} diff --git a/requirements.txt b/requirements.txt index e112c21..1e32cde 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,9 @@ nose oslo.config>=1.6.0 # Apache-2.0 pbr>=0.6,!=0.7,<1.0 oslo.utils +PrettyTable>=0.7,<0.8 +requests>=2.2.0,!=2.4.0 +simplejson>=2.2.0 #TODO(???): move this fix into fuel-ostf python-muranoclient diff --git a/setup.cfg b/setup.cfg index b37c9e5..53ac31f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,8 +25,9 @@ domain = cloudv_ostf_adapter [entry_points] console_scripts = - cloudvalidation = cloudv_ostf_adapter.cmd.cloudv_runner:main - cloudvalidation-server = cloudv_ostf_adapter.server:main + cloudvalidation-cli = cloudv_ostf_adapter.cmd.cli:main + cloudvalidation-server = cloudv_ostf_adapter.cmd.server:main + cloudvalidation = cloudv_ostf_adapter.cmd.client:main [global] setup-hooks =