From b3e40f026d655501bfa581452c447784604ecb05 Mon Sep 17 00:00:00 2001 From: xudan Date: Fri, 6 Jul 2018 05:16:40 -0400 Subject: Move all web portal code to the new repo dovetail-webportal This is only the first step to simply copy the file here. There still need some more work to make sure all work well. All the changes will be submitted with other patches to make it easily to review. JIRA: DOVETAIL-671 Change-Id: I64d32a9df562184166b6199e2719f298687d1a0a Signed-off-by: xudan --- opnfv_testapi/__init__.py | 8 + opnfv_testapi/cmd/__init__.py | 8 + opnfv_testapi/cmd/server.py | 64 ++++ opnfv_testapi/common/__init__.py | 8 + opnfv_testapi/common/check.py | 114 ++++++ opnfv_testapi/common/config.py | 79 ++++ opnfv_testapi/common/message.py | 54 +++ opnfv_testapi/common/raises.py | 39 ++ opnfv_testapi/common/utils.py | 43 +++ opnfv_testapi/db/__init__.py | 0 opnfv_testapi/db/api.py | 38 ++ opnfv_testapi/resources/__init__.py | 8 + opnfv_testapi/resources/application_handlers.py | 233 ++++++++++++ opnfv_testapi/resources/application_models.py | 39 ++ opnfv_testapi/resources/handlers.py | 331 +++++++++++++++++ opnfv_testapi/resources/models.py | 115 ++++++ opnfv_testapi/resources/pod_handlers.py | 78 ++++ opnfv_testapi/resources/pod_models.py | 52 +++ opnfv_testapi/resources/project_handlers.py | 86 +++++ opnfv_testapi/resources/project_models.py | 48 +++ opnfv_testapi/resources/result_handlers.py | 308 ++++++++++++++++ opnfv_testapi/resources/result_models.py | 133 +++++++ opnfv_testapi/resources/scenario_handlers.py | 282 ++++++++++++++ opnfv_testapi/resources/scenario_models.py | 204 ++++++++++ opnfv_testapi/resources/sut_handlers.py | 112 ++++++ opnfv_testapi/resources/sut_models.py | 31 ++ opnfv_testapi/resources/test_handlers.py | 307 +++++++++++++++ opnfv_testapi/resources/test_models.py | 90 +++++ opnfv_testapi/resources/testcase_handlers.py | 103 ++++++ opnfv_testapi/resources/testcase_models.py | 95 +++++ opnfv_testapi/router/__init__.py | 9 + opnfv_testapi/router/url_mappings.py | 45 +++ opnfv_testapi/tests/__init__.py | 1 + opnfv_testapi/tests/unit/__init__.py | 9 + opnfv_testapi/tests/unit/common/__init__.py | 0 opnfv_testapi/tests/unit/common/noparam.ini | 16 + opnfv_testapi/tests/unit/common/normal.ini | 17 + opnfv_testapi/tests/unit/common/nosection.ini | 11 + opnfv_testapi/tests/unit/common/notboolean.ini | 17 + opnfv_testapi/tests/unit/common/notint.ini | 17 + opnfv_testapi/tests/unit/common/test_config.py | 15 + opnfv_testapi/tests/unit/conftest.py | 8 + opnfv_testapi/tests/unit/executor.py | 97 +++++ opnfv_testapi/tests/unit/fake_pymongo.py | 284 ++++++++++++++ opnfv_testapi/tests/unit/resources/__init__.py | 0 .../tests/unit/resources/scenario-c1.json | 38 ++ .../tests/unit/resources/scenario-c2.json | 73 ++++ opnfv_testapi/tests/unit/resources/test_base.py | 161 ++++++++ .../tests/unit/resources/test_fake_pymongo.py | 123 +++++++ opnfv_testapi/tests/unit/resources/test_pod.py | 90 +++++ opnfv_testapi/tests/unit/resources/test_project.py | 137 +++++++ opnfv_testapi/tests/unit/resources/test_result.py | 410 +++++++++++++++++++++ .../tests/unit/resources/test_scenario.py | 360 ++++++++++++++++++ .../tests/unit/resources/test_testcase.py | 201 ++++++++++ opnfv_testapi/tests/unit/resources/test_token.py | 114 ++++++ opnfv_testapi/tests/unit/resources/test_version.py | 36 ++ opnfv_testapi/tornado_swagger/README.md | 273 ++++++++++++++ opnfv_testapi/tornado_swagger/__init__.py | 8 + opnfv_testapi/tornado_swagger/handlers.py | 38 ++ opnfv_testapi/tornado_swagger/settings.py | 25 ++ opnfv_testapi/tornado_swagger/swagger.py | 291 +++++++++++++++ opnfv_testapi/tornado_swagger/views.py | 134 +++++++ opnfv_testapi/ui/__init__.py | 0 opnfv_testapi/ui/auth/__init__.py | 0 opnfv_testapi/ui/auth/base.py | 35 ++ opnfv_testapi/ui/auth/constants.py | 18 + opnfv_testapi/ui/auth/jira_util.py | 66 ++++ opnfv_testapi/ui/auth/rsa.pem | 27 ++ opnfv_testapi/ui/auth/sign.py | 281 ++++++++++++++ opnfv_testapi/ui/auth/user.py | 35 ++ opnfv_testapi/ui/root.py | 10 + 71 files changed, 6640 insertions(+) create mode 100644 opnfv_testapi/__init__.py create mode 100644 opnfv_testapi/cmd/__init__.py create mode 100644 opnfv_testapi/cmd/server.py create mode 100644 opnfv_testapi/common/__init__.py create mode 100644 opnfv_testapi/common/check.py create mode 100644 opnfv_testapi/common/config.py create mode 100644 opnfv_testapi/common/message.py create mode 100644 opnfv_testapi/common/raises.py create mode 100644 opnfv_testapi/common/utils.py create mode 100644 opnfv_testapi/db/__init__.py create mode 100644 opnfv_testapi/db/api.py create mode 100644 opnfv_testapi/resources/__init__.py create mode 100644 opnfv_testapi/resources/application_handlers.py create mode 100644 opnfv_testapi/resources/application_models.py create mode 100644 opnfv_testapi/resources/handlers.py create mode 100644 opnfv_testapi/resources/models.py create mode 100644 opnfv_testapi/resources/pod_handlers.py create mode 100644 opnfv_testapi/resources/pod_models.py create mode 100644 opnfv_testapi/resources/project_handlers.py create mode 100644 opnfv_testapi/resources/project_models.py create mode 100644 opnfv_testapi/resources/result_handlers.py create mode 100644 opnfv_testapi/resources/result_models.py create mode 100644 opnfv_testapi/resources/scenario_handlers.py create mode 100644 opnfv_testapi/resources/scenario_models.py create mode 100644 opnfv_testapi/resources/sut_handlers.py create mode 100644 opnfv_testapi/resources/sut_models.py create mode 100644 opnfv_testapi/resources/test_handlers.py create mode 100644 opnfv_testapi/resources/test_models.py create mode 100644 opnfv_testapi/resources/testcase_handlers.py create mode 100644 opnfv_testapi/resources/testcase_models.py create mode 100644 opnfv_testapi/router/__init__.py create mode 100644 opnfv_testapi/router/url_mappings.py create mode 100644 opnfv_testapi/tests/__init__.py create mode 100644 opnfv_testapi/tests/unit/__init__.py create mode 100644 opnfv_testapi/tests/unit/common/__init__.py create mode 100644 opnfv_testapi/tests/unit/common/noparam.ini create mode 100644 opnfv_testapi/tests/unit/common/normal.ini create mode 100644 opnfv_testapi/tests/unit/common/nosection.ini create mode 100644 opnfv_testapi/tests/unit/common/notboolean.ini create mode 100644 opnfv_testapi/tests/unit/common/notint.ini create mode 100644 opnfv_testapi/tests/unit/common/test_config.py create mode 100644 opnfv_testapi/tests/unit/conftest.py create mode 100644 opnfv_testapi/tests/unit/executor.py create mode 100644 opnfv_testapi/tests/unit/fake_pymongo.py create mode 100644 opnfv_testapi/tests/unit/resources/__init__.py create mode 100644 opnfv_testapi/tests/unit/resources/scenario-c1.json create mode 100644 opnfv_testapi/tests/unit/resources/scenario-c2.json create mode 100644 opnfv_testapi/tests/unit/resources/test_base.py create mode 100644 opnfv_testapi/tests/unit/resources/test_fake_pymongo.py create mode 100644 opnfv_testapi/tests/unit/resources/test_pod.py create mode 100644 opnfv_testapi/tests/unit/resources/test_project.py create mode 100644 opnfv_testapi/tests/unit/resources/test_result.py create mode 100644 opnfv_testapi/tests/unit/resources/test_scenario.py create mode 100644 opnfv_testapi/tests/unit/resources/test_testcase.py create mode 100644 opnfv_testapi/tests/unit/resources/test_token.py create mode 100644 opnfv_testapi/tests/unit/resources/test_version.py create mode 100644 opnfv_testapi/tornado_swagger/README.md create mode 100644 opnfv_testapi/tornado_swagger/__init__.py create mode 100644 opnfv_testapi/tornado_swagger/handlers.py create mode 100644 opnfv_testapi/tornado_swagger/settings.py create mode 100644 opnfv_testapi/tornado_swagger/swagger.py create mode 100644 opnfv_testapi/tornado_swagger/views.py create mode 100644 opnfv_testapi/ui/__init__.py create mode 100644 opnfv_testapi/ui/auth/__init__.py create mode 100644 opnfv_testapi/ui/auth/base.py create mode 100644 opnfv_testapi/ui/auth/constants.py create mode 100644 opnfv_testapi/ui/auth/jira_util.py create mode 100644 opnfv_testapi/ui/auth/rsa.pem create mode 100644 opnfv_testapi/ui/auth/sign.py create mode 100644 opnfv_testapi/ui/auth/user.py create mode 100644 opnfv_testapi/ui/root.py (limited to 'opnfv_testapi') diff --git a/opnfv_testapi/__init__.py b/opnfv_testapi/__init__.py new file mode 100644 index 0000000..363bc38 --- /dev/null +++ b/opnfv_testapi/__init__.py @@ -0,0 +1,8 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## diff --git a/opnfv_testapi/cmd/__init__.py b/opnfv_testapi/cmd/__init__.py new file mode 100644 index 0000000..363bc38 --- /dev/null +++ b/opnfv_testapi/cmd/__init__.py @@ -0,0 +1,8 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## diff --git a/opnfv_testapi/cmd/server.py b/opnfv_testapi/cmd/server.py new file mode 100644 index 0000000..d503c8a --- /dev/null +++ b/opnfv_testapi/cmd/server.py @@ -0,0 +1,64 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +""" +Pre-requisites: + pip install motor + pip install tornado + +We can launch the API with this file + +TODOs : + - logging + - json args validation with schemes + - POST/PUT/DELETE for PODs + - POST/PUT/GET/DELETE for installers, platforms (enrich results info) + - count cases for GET on projects + - count results for GET on cases + - include objects + - swagger documentation + - setup file + - results pagination + - unit tests + +""" + +import tornado.ioloop +import logging + +from opnfv_testapi.common.config import CONF +from opnfv_testapi.router import url_mappings +from opnfv_testapi.tornado_swagger import swagger + +my_logger = logging.getLogger() +handler = logging.handlers.RotatingFileHandler( + CONF.api_log_file, maxBytes=20000000, backupCount=50) +my_logger.setLevel(logging.DEBUG) +my_logger.addHandler(handler) + + +def make_app(): + swagger.docs(base_url=CONF.swagger_base_url, + static_path=CONF.static_path) + return swagger.Application( + url_mappings.mappings, + debug=CONF.api_debug, + auth=CONF.api_authenticate, + cookie_secret='opnfv-testapi', + ) + + +def main(): + application = make_app() + application.listen(CONF.api_port) + tornado.ioloop.IOLoop.current().start() + + +if __name__ == "__main__": + main() diff --git a/opnfv_testapi/common/__init__.py b/opnfv_testapi/common/__init__.py new file mode 100644 index 0000000..05c0c93 --- /dev/null +++ b/opnfv_testapi/common/__init__.py @@ -0,0 +1,8 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## diff --git a/opnfv_testapi/common/check.py b/opnfv_testapi/common/check.py new file mode 100644 index 0000000..24ba876 --- /dev/null +++ b/opnfv_testapi/common/check.py @@ -0,0 +1,114 @@ +############################################################################## +# Copyright (c) 2017 ZTE Corp +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import functools + +from tornado import gen +from tornado import web + +from opnfv_testapi.common import message +from opnfv_testapi.common import raises +from opnfv_testapi.db import api as dbapi + + +def authenticate(method): + @web.asynchronous + @gen.coroutine + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + if self.auth: + try: + token = self.request.headers['X-Auth-Token'] + except KeyError: + raises.Unauthorized(message.unauthorized()) + query = {'access_token': token} + check = yield dbapi.db_find_one('tokens', query) + if not check: + raises.Forbidden(message.invalid_token()) + ret = yield gen.coroutine(method)(self, *args, **kwargs) + raise gen.Return(ret) + return wrapper + + +def not_exist(xstep): + @functools.wraps(xstep) + def wrap(self, *args, **kwargs): + query = kwargs.get('query') + data = yield dbapi.db_find_one(self.table, query) + if not data: + raises.NotFound(message.not_found(self.table, query)) + ret = yield gen.coroutine(xstep)(self, data, *args, **kwargs) + raise gen.Return(ret) + + return wrap + + +def no_body(xstep): + @functools.wraps(xstep) + def wrap(self, *args, **kwargs): + if self.json_args is None: + raises.BadRequest(message.no_body()) + ret = yield gen.coroutine(xstep)(self, *args, **kwargs) + raise gen.Return(ret) + + return wrap + + +def miss_fields(xstep): + @functools.wraps(xstep) + def wrap(self, *args, **kwargs): + fields = kwargs.pop('miss_fields', []) + if fields: + for miss in fields: + miss_data = self.json_args.get(miss) + if miss_data is None or miss_data == '': + raises.BadRequest(message.missing(miss)) + ret = yield gen.coroutine(xstep)(self, *args, **kwargs) + raise gen.Return(ret) + return wrap + + +def carriers_exist(xstep): + @functools.wraps(xstep) + def wrap(self, *args, **kwargs): + carriers = kwargs.pop('carriers', {}) + if carriers: + for table, query in carriers: + exist = yield dbapi.db_find_one(table, query()) + if not exist: + raises.Forbidden(message.not_found(table, query())) + ret = yield gen.coroutine(xstep)(self, *args, **kwargs) + raise gen.Return(ret) + return wrap + + +def new_not_exists(xstep): + @functools.wraps(xstep) + def wrap(self, *args, **kwargs): + query = kwargs.get('query') + if query: + to_data = yield dbapi.db_find_one(self.table, query()) + if to_data: + raises.Forbidden(message.exist(self.table, query())) + ret = yield gen.coroutine(xstep)(self, *args, **kwargs) + raise gen.Return(ret) + return wrap + + +def updated_one_not_exist(xstep): + @functools.wraps(xstep) + def wrap(self, data, *args, **kwargs): + db_keys = kwargs.pop('db_keys', []) + query = self._update_query(db_keys, data) + if query: + to_data = yield dbapi.db_find_one(self.table, query) + if to_data: + raises.Forbidden(message.exist(self.table, query)) + ret = yield gen.coroutine(xstep)(self, data, *args, **kwargs) + raise gen.Return(ret) + return wrap diff --git a/opnfv_testapi/common/config.py b/opnfv_testapi/common/config.py new file mode 100644 index 0000000..75dbc35 --- /dev/null +++ b/opnfv_testapi/common/config.py @@ -0,0 +1,79 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +# feng.xiaowei@zte.com.cn remove prepare_put_request 5-30-2016 +############################################################################## +import ConfigParser +import argparse +import os +import sys + + +class Config(object): + + def __init__(self): + self.config_file = None + self._set_config_file() + self._parse() + self._parse_per_page() + self.static_path = os.path.join( + os.path.dirname(os.path.normpath(__file__)), + os.pardir, + 'static') + self.base_path = "/home/testapi" + + def _parse(self): + if not os.path.exists(self.config_file): + raise Exception("%s not found" % self.config_file) + + config = ConfigParser.RawConfigParser() + config.read(self.config_file) + self._parse_section(config) + + def _parse_section(self, config): + [self._parse_item(config, section) for section in (config.sections())] + + def _parse_item(self, config, section): + [setattr(self, '{}_{}'.format(section, k), self._parse_value(v)) + for k, v in config.items(section)] + + def _parse_per_page(self): + if not hasattr(self, 'api_results_per_page'): + self.api_results_per_page = 20 + + @staticmethod + def _parse_value(value): + try: + value = int(value) + except: + if str(value).lower() == 'true': + value = True + elif str(value).lower() == 'false': + value = False + return value + + def _set_config_file(self): + if not self._set_sys_config_file(): + self._set_default_config_file() + + def _set_sys_config_file(self): + parser = argparse.ArgumentParser() + parser.add_argument("-c", "--config-file", dest='config_file', + help="Config file location", metavar="FILE") + args, _ = parser.parse_known_args(sys.argv) + try: + self.config_file = args.config_file + finally: + return self.config_file is not None + + def _set_default_config_file(self): + is_venv = os.getenv('VIRTUAL_ENV') + self.config_file = os.path.join('/' if not is_venv else is_venv, + 'etc/opnfv_testapi/config.ini') + + +CONF = Config() diff --git a/opnfv_testapi/common/message.py b/opnfv_testapi/common/message.py new file mode 100644 index 0000000..61ce03d --- /dev/null +++ b/opnfv_testapi/common/message.py @@ -0,0 +1,54 @@ +############################################################################## +# Copyright (c) 2017 ZTE Corp +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +not_found_base = 'Could Not Found' +exist_base = 'Already Exists' + + +def key_error(key): + return "KeyError: '{}'".format(key) + + +def no_body(): + return 'No Body' + + +def not_found(key, value): + return '{} {} [{}]'.format(not_found_base, key, value) + + +def missing(name): + return '{} Missing'.format(name) + + +def exist(key, value): + return '{} [{}] {}'.format(key, value, exist_base) + + +def bad_format(error): + return 'Bad Format [{}]'.format(error) + + +def unauthorized(): + return 'No Authentication Header' + + +def invalid_token(): + return 'Invalid Token' + + +def no_update(): + return 'Nothing to update' + + +def must_int(name): + return '{} must be int'.format(name) + + +def no_auth(): + return 'No permission to operate. Please ask Administrator for details.' diff --git a/opnfv_testapi/common/raises.py b/opnfv_testapi/common/raises.py new file mode 100644 index 0000000..ec6b8a5 --- /dev/null +++ b/opnfv_testapi/common/raises.py @@ -0,0 +1,39 @@ +############################################################################## +# Copyright (c) 2017 ZTE Corp +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import httplib + +from tornado import web + + +class Raiser(object): + code = httplib.OK + + def __init__(self, reason): + raise web.HTTPError(self.code, reason) + + +class BadRequest(Raiser): + code = httplib.BAD_REQUEST + + +class Forbidden(Raiser): + code = httplib.FORBIDDEN + + +class NotFound(Raiser): + code = httplib.NOT_FOUND + + +class Unauthorized(Raiser): + code = httplib.UNAUTHORIZED + + +class CodeTBD(object): + def __init__(self, code, reason): + raise web.HTTPError(code, reason) diff --git a/opnfv_testapi/common/utils.py b/opnfv_testapi/common/utils.py new file mode 100644 index 0000000..107c709 --- /dev/null +++ b/opnfv_testapi/common/utils.py @@ -0,0 +1,43 @@ +import logging +import smtplib +from email.mime.text import MIMEText + +LOG = logging.getLogger(__name__) +LOG.setLevel(logging.DEBUG) + + +def send_email(subject, content): + MAIL_LIST = ['cvp@opnfv.org'] + HOST = "smtp.gmail.com" + USER = "opnfv.cvp" + PASSWD = "opnfv@cvp" + + sender = 'cvp<{}@gmail.com>'.format(USER) + msg = MIMEText(content, _subtype='plain') + msg['Subject'] = subject + msg['From'] = sender + msg['To'] = ";".join(MAIL_LIST) + + _send_email(HOST, sender, USER, PASSWD, MAIL_LIST, msg) + + +def _send_email(host, + sender, + user, + passwd, + receivers, + msg): + + client = smtplib.SMTP() + try: + client.connect(host, 25) + LOG.debug('Success to connect server') + client.starttls() + client.login(user, passwd) + LOG.debug('Success to login') + LOG.debug('Start to sending email') + client.sendmail(sender, receivers, msg.as_string()) + client.close() + except Exception: + LOG.exception('Error when sending email') + raise diff --git a/opnfv_testapi/db/__init__.py b/opnfv_testapi/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opnfv_testapi/db/api.py b/opnfv_testapi/db/api.py new file mode 100644 index 0000000..c057480 --- /dev/null +++ b/opnfv_testapi/db/api.py @@ -0,0 +1,38 @@ +import motor + +from opnfv_testapi.common.config import CONF + +DB = motor.MotorClient(CONF.mongo_url)[CONF.mongo_dbname] + + +def db_update(collection, query, update_req): + return _eval_db(collection, 'update', query, update_req, check_keys=False) + + +def db_delete(collection, query): + return _eval_db(collection, 'remove', query) + + +def db_aggregate(collection, pipelines): + return _eval_db(collection, 'aggregate', pipelines, allowDiskUse=True) + + +def db_list(collection, query): + return _eval_db(collection, 'find', query) + + +def db_save(collection, data): + return _eval_db(collection, 'insert', data, check_keys=False) + + +def db_find_one(collection, query): + return _eval_db(collection, 'find_one', query) + + +def _eval_db(collection, method, *args, **kwargs): + exec_collection = DB.__getattr__(collection) + return exec_collection.__getattribute__(method)(*args, **kwargs) + + +def _eval_db_find_one(query, table=None): + return _eval_db(table, 'find_one', query) diff --git a/opnfv_testapi/resources/__init__.py b/opnfv_testapi/resources/__init__.py new file mode 100644 index 0000000..05c0c93 --- /dev/null +++ b/opnfv_testapi/resources/__init__.py @@ -0,0 +1,8 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## diff --git a/opnfv_testapi/resources/application_handlers.py b/opnfv_testapi/resources/application_handlers.py new file mode 100644 index 0000000..258c1aa --- /dev/null +++ b/opnfv_testapi/resources/application_handlers.py @@ -0,0 +1,233 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import logging +import json + +from tornado import web +from tornado import gen +from bson import objectid + +from opnfv_testapi.common.config import CONF +from opnfv_testapi.common import utils +from opnfv_testapi.resources import handlers +from opnfv_testapi.resources import application_models +from opnfv_testapi.tornado_swagger import swagger +from opnfv_testapi.ui.auth import constants as auth_const + + +class GenericApplicationHandler(handlers.GenericApiHandler): + def __init__(self, application, request, **kwargs): + super(GenericApplicationHandler, self).__init__(application, + request, + **kwargs) + self.table = "applications" + self.table_cls = application_models.Application + + +class ApplicationsLogoHandler(GenericApplicationHandler): + @web.asynchronous + @gen.coroutine + def post(self): + role = self.get_secure_cookie(auth_const.ROLE) + if role.find('administrator') == -1: + msg = 'Only administrator is allowed to upload logos' + self.finish_request({'code': '-1', 'msg': msg}) + return + + fileinfo = self.request.files['file'][0] + fname = fileinfo['filename'] + location = '3rd_party/static/testapi-ui/assets/img/' + fh = open(location + fname, 'w') + fh.write(fileinfo['body']) + msg = 'Successfully uploaded logo: ' + fname + resp = {'code': '1', 'msg': msg} + self.finish_request(resp) + + +class ApplicationsGetLogoHandler(GenericApplicationHandler): + def get(self, filename): + location = '3rd_party/static/testapi-ui/assets/img/' + filename + self.set_header('Content-Type', 'application/force-download') + self.set_header('Content-Disposition', + 'attachment; filename=%s' % filename) + try: + with open(location, "rb") as f: + try: + while True: + _buffer = f.read(4096) + if _buffer: + self.write(_buffer) + else: + f.close() + self.finish() + return + except Exception: + raise web.HTTPError(404) + except Exception: + raise web.HTTPError(500) + + +class ApplicationsCLHandler(GenericApplicationHandler): + @swagger.operation(nickname="queryApplications") + @web.asynchronous + @gen.coroutine + def get(self): + """ + @description: Retrieve result(s) for a application project + on a specific pod. + @notes: Retrieve application. + Available filters for this request are : + - id : Application id + - period : x last days, incompatible with from/to + - from : starting time in 2016-01-01 or 2016-01-01 00:01:23 + - to : ending time in 2016-01-01 or 2016-01-01 00:01:23 + - signed : get logined user result + + @return 200: all application results consist with query, + empty list if no result is found + @rtype: L{Applications} + """ + def descend_limit(): + descend = self.get_query_argument('descend', 'true') + return -1 if descend.lower() == 'true' else 1 + + def last_limit(): + return self.get_int('last', self.get_query_argument('last', 0)) + + def page_limit(): + return self.get_int('page', self.get_query_argument('page', 0)) + + limitations = { + 'sort': {'_id': descend_limit()}, + 'last': last_limit(), + 'page': page_limit(), + 'per_page': CONF.api_results_per_page + } + + query = yield self.set_query() + yield self._list(query=query, **limitations) + logging.debug('list end') + + @swagger.operation(nickname="createApplication") + @web.asynchronous + def post(self): + """ + @description: create a application + @param body: application to be created + @type body: L{ApplicationCreateRequest} + @in body: body + @rtype: L{CreateResponse} + @return 200: application is created. + @raise 404: pod/project/applicationcase not exist + @raise 400: body/pod_name/project_name/case_name not provided + """ + openid = self.get_secure_cookie(auth_const.OPENID) + if openid: + self.json_args['owner'] = openid + + self._post() + + @gen.coroutine + def _post(self): + miss_fields = [] + carriers = [] + + role = self.get_secure_cookie(auth_const.ROLE) + if role.find('administrator') == -1: + self.finish_request({'code': '403', 'msg': 'Only administrator \ + is allowed to submit application.'}) + return + + query = {"openid": self.json_args['user_id']} + table = "users" + ret, msg = yield self._check_if_exists(table=table, query=query) + logging.debug('ret:%s', ret) + if not ret: + self.finish_request({'code': '403', 'msg': msg}) + return + self._create(miss_fields=miss_fields, carriers=carriers) + + self._send_email() + + def _send_email(self): + + data = self.table_cls.from_dict(self.json_args) + subject = "[OPNFV CVP]New OPNFV CVP Application Submission" + content = """Hi CVP Reviewer, + +This is a new application: + + Organization Name: {}, + Organization Website: {}, + Product Name: {}, + Product Specifications: {}, + Product Documentation: {}, + Product Categories: {}, + Primary Name: {}, + Primary Email: {}, + Primary Address: {}, + Primary Phone: {}, + User ID Type: {}, + User ID: {} + +Best Regards, +CVP Team + """.format(data.organization_name, + data.organization_web, + data.product_name, + data.product_spec, + data.product_documentation, + data.product_categories, + data.prim_name, + data.prim_email, + data.prim_address, + data.prim_phone, + data.id_type, + data.user_id) + + utils.send_email(subject, content) + + +class ApplicationsGURHandler(GenericApplicationHandler): + @swagger.operation(nickname="deleteAppById") + def delete(self, id): + query = {'_id': objectid.ObjectId(id)} + self._delete(query=query) + + @swagger.operation(nickname="updateApplicationById") + def put(self, application_id): + """ + @description: update a single application by id + @param body: fields to be updated + @type body: L{ApplicationUpdateRequest} + @in body: body + @rtype: L{Application} + @return 200: update success + @raise 404: Application not exist + @raise 403: nothing to update + """ + data = json.loads(self.request.body) + item = data.get('item') + value = data.get(item) + logging.debug('%s:%s', item, value) + try: + self.update(application_id, item, value) + except Exception as e: + logging.error('except:%s', e) + return + + @web.asynchronous + @gen.coroutine + def update(self, application_id, item, value): + self.json_args = {} + self.json_args[item] = value + query = {'_id': application_id, 'owner': + self.get_secure_cookie(auth_const.OPENID)} + db_keys = ['_id', 'owner'] + self._update(query=query, db_keys=db_keys) diff --git a/opnfv_testapi/resources/application_models.py b/opnfv_testapi/resources/application_models.py new file mode 100644 index 0000000..e2bb652 --- /dev/null +++ b/opnfv_testapi/resources/application_models.py @@ -0,0 +1,39 @@ +############################################################################## +# Copyright (c) 2015 +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from opnfv_testapi.resources import models +from opnfv_testapi.tornado_swagger import swagger + +from datetime import datetime + + +@swagger.model() +class Application(models.ModelBase): + """ + @property trust_indicator: used for long duration test case + @ptype trust_indicator: L{TI} + """ + def __init__(self, _id=None, owner=None, status="created", + creation_date=[], trust_indicator=None): + self._id = _id + self.owner = owner + self.creation_date = datetime.now() + self.status = status + + +@swagger.model() +class Applications(models.ModelBase): + """ + @property applications: + @ptype tests: C{list} of L{Application} + """ + def __init__(self): + self.applications = list() + + @staticmethod + def attr_parser(): + return {'applications': Application} diff --git a/opnfv_testapi/resources/handlers.py b/opnfv_testapi/resources/handlers.py new file mode 100644 index 0000000..9b156e1 --- /dev/null +++ b/opnfv_testapi/resources/handlers.py @@ -0,0 +1,331 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +# feng.xiaowei@zte.com.cn refactor db.pod to db.pods 5-19-2016 +# feng.xiaowei@zte.com.cn refactor test_project to project 5-19-2016 +# feng.xiaowei@zte.com.cn refactor response body 5-19-2016 +# feng.xiaowei@zte.com.cn refactor pod/project response info 5-19-2016 +# feng.xiaowei@zte.com.cn refactor testcase related handler 5-20-2016 +# feng.xiaowei@zte.com.cn refactor result related handler 5-23-2016 +# feng.xiaowei@zte.com.cn refactor dashboard related handler 5-24-2016 +# feng.xiaowei@zte.com.cn add methods to GenericApiHandler 5-26-2016 +# feng.xiaowei@zte.com.cn remove PodHandler 5-26-2016 +# feng.xiaowei@zte.com.cn remove ProjectHandler 5-26-2016 +# feng.xiaowei@zte.com.cn remove TestcaseHandler 5-27-2016 +# feng.xiaowei@zte.com.cn remove ResultHandler 5-29-2016 +# feng.xiaowei@zte.com.cn remove DashboardHandler 5-30-2016 +############################################################################## + +import json +from datetime import datetime +from datetime import timedelta + +import logging +from tornado import gen +from tornado import web + +from opnfv_testapi.common import check +from opnfv_testapi.common import message +from opnfv_testapi.common import raises +from opnfv_testapi.db import api as dbapi +from opnfv_testapi.resources import models +from opnfv_testapi.tornado_swagger import swagger +from opnfv_testapi.ui.auth import constants as auth_const + +DEFAULT_REPRESENTATION = "application/json" + + +class GenericApiHandler(web.RequestHandler): + def __init__(self, application, request, **kwargs): + super(GenericApiHandler, self).__init__(application, request, **kwargs) + self.json_args = None + self.table = None + self.table_cls = None + self.db_projects = 'projects' + self.db_pods = 'pods' + self.db_testcases = 'testcases' + self.db_results = 'results' + self.db_scenarios = 'scenarios' + self.auth = self.settings["auth"] + + def get_int(self, key, value): + try: + value = int(value) + except: + raises.BadRequest(message.must_int(key)) + return value + + @gen.coroutine + def set_query(self): + query = dict() + date_range = dict() + for k in self.request.query_arguments.keys(): + v = self.get_query_argument(k) + if k == 'period': + v = self.get_int(k, v) + if v > 0: + period = datetime.now() - timedelta(days=v) + obj = {"$gte": str(period)} + query['start_date'] = obj + elif k == 'from': + date_range.update({'$gte': str(v)}) + elif k == 'to': + date_range.update({'$lt': str(v)}) + elif k == 'signed': + openid = self.get_secure_cookie(auth_const.OPENID) + user = yield dbapi.db_find_one("users", {'openid': openid}) + role = self.get_secure_cookie(auth_const.ROLE) + logging.info('role:%s', role) + if role: + query['$or'] = [ + { + "shared": { + "$elemMatch": {"$eq": openid} + } + }, + {"owner": openid}, + { + "shared": { + "$elemMatch": {"$eq": user.get("email")} + } + } + ] + + if role.find("reviewer") != -1: + query['$or'].append({"status": {"$ne": "private"}}) + elif k not in ['last', 'page', 'descend', 'per_page']: + query[k] = v + if date_range: + query['start_date'] = date_range + + # if $lt is not provided, + # empty/None/null/'' start_date will also be returned + if 'start_date' in query and '$lt' not in query['start_date']: + query['start_date'].update({'$lt': str(datetime.now())}) + + logging.debug("query:%s", query) + raise gen.Return((query)) + + def prepare(self): + if self.request.method != "GET" and self.request.method != "DELETE": + if self.request.headers.get("Content-Type") is not None: + if self.request.headers["Content-Type"].startswith( + DEFAULT_REPRESENTATION): + try: + self.json_args = json.loads(self.request.body) + except (ValueError, KeyError, TypeError) as error: + raises.BadRequest(message.bad_format(str(error))) + + def finish_request(self, json_object=None): + if json_object: + self.write(json.dumps(json_object)) + self.set_header("Content-Type", DEFAULT_REPRESENTATION) + self.finish() + + def _create_response(self, resource): + href = self.request.full_url() + '/' + str(resource) + return models.CreateResponse(href=href).format() + + def format_data(self, data): + cls_data = self.table_cls.from_dict(data) + return cls_data.format_http() + + @gen.coroutine + @check.no_body + @check.miss_fields + @check.carriers_exist + @check.new_not_exists + def _inner_create(self, **kwargs): + data = self.table_cls.from_dict(self.json_args) + for k, v in kwargs.iteritems(): + if k != 'query': + data.__setattr__(k, v) + + if self.table != 'results': + data.creation_date = datetime.now() + _id = yield dbapi.db_save(self.table, data.format()) + logging.warning("_id:%s", _id) + raise gen.Return(_id) + + def _create_only(self, **kwargs): + resource = self._inner_create(**kwargs) + logging.warning("resource:%s", resource) + + @check.authenticate + @check.no_body + @check.miss_fields + @check.carriers_exist + @check.new_not_exists + def _create(self, **kwargs): + # resource = self._inner_create(**kwargs) + data = self.table_cls.from_dict(self.json_args) + for k, v in kwargs.iteritems(): + if k != 'query': + data.__setattr__(k, v) + + if self.table != 'results': + data.creation_date = datetime.now() + _id = yield dbapi.db_save(self.table, data.format()) + if 'name' in self.json_args: + resource = data.name + else: + resource = _id + + self.finish_request(self._create_response(resource)) + + @gen.coroutine + def _check_if_exists(self, *args, **kwargs): + query = kwargs['query'] + table = kwargs['table'] + if query and table: + data = yield dbapi.db_find_one(table, query) + if data: + raise gen.Return((True, 'Data alreay exists. %s' % (query))) + raise gen.Return((False, 'Data does not exist. %s' % (query))) + + # @web.asynchronous + @gen.coroutine + def _list(self, query=None, res_op=None, *args, **kwargs): + logging.debug("_list query:%s", query) + sort = kwargs.get('sort') + page = kwargs.get('page', 0) + last = kwargs.get('last', 0) + per_page = kwargs.get('per_page', 0) + if query is None: + query = {} + + total_pages = 0 + if page > 0: + cursor = dbapi.db_list(self.table, query) + records_count = yield cursor.count() + total_pages = self._calc_total_pages(records_count, + last, + page, + per_page) + pipelines = self._set_pipelines(query, sort, last, page, per_page) + cursor = dbapi.db_aggregate(self.table, pipelines) + data = list() + while (yield cursor.fetch_next): + data.append(self.format_data(cursor.next_object())) + if res_op is None: + res = {self.table: data} + else: + res = res_op(data, *args) + if page > 0: + res.update({ + 'pagination': { + 'current_page': kwargs.get('page'), + 'total_pages': total_pages + } + }) + self.finish_request(res) + logging.debug('_list end') + + @staticmethod + def _calc_total_pages(records_count, last, page, per_page): + logging.debug("totalItems:%d per_page:%d", records_count, per_page) + records_nr = records_count + if (records_count > last) and (last > 0): + records_nr = last + + total_pages, remainder = divmod(records_nr, per_page) + if remainder > 0: + total_pages += 1 + if page > 1 and page > total_pages: + raises.BadRequest( + 'Request page > total_pages [{}]'.format(total_pages)) + return total_pages + + @staticmethod + def _set_pipelines(query, sort, last, page, per_page): + pipelines = list() + if query: + pipelines.append({'$match': query}) + if sort: + pipelines.append({'$sort': sort}) + + if page > 0: + pipelines.append({'$skip': (page - 1) * per_page}) + pipelines.append({'$limit': per_page}) + elif last > 0: + pipelines.append({'$limit': last}) + + return pipelines + + @web.asynchronous + @gen.coroutine + @check.not_exist + def _get_one(self, data, query=None): + self.finish_request(self.format_data(data)) + + @check.authenticate + @check.not_exist + def _delete(self, data, query=None): + yield dbapi.db_delete(self.table, query) + self.finish_request() + + @check.authenticate + @check.no_body + @check.not_exist + @check.updated_one_not_exist + def _update(self, data, query=None, **kwargs): + logging.debug("_update") + data = self.table_cls.from_dict(data) + update_req = self._update_requests(data) + yield dbapi.db_update(self.table, query, update_req) + update_req['_id'] = str(data._id) + self.finish_request(update_req) + + def _update_requests(self, data): + request = dict() + for k, v in self.json_args.iteritems(): + request = self._update_request(request, k, v, + data.__getattribute__(k)) + if not request: + raises.Forbidden(message.no_update()) + + edit_request = data.format() + edit_request.update(request) + return edit_request + + @staticmethod + def _update_request(edit_request, key, new_value, old_value): + """ + This function serves to prepare the elements in the update request. + We try to avoid replace the exact values in the db + edit_request should be a dict in which we add an entry (key) after + comparing values + """ + if not (new_value is None): + if new_value != old_value: + edit_request[key] = new_value + + return edit_request + + def _update_query(self, keys, data): + query = dict() + equal = True + for key in keys: + new = self.json_args.get(key) + old = data.get(key) + if new is None: + new = old + elif new != old: + equal = False + query[key] = new + return query if not equal else dict() + + +class VersionHandler(GenericApiHandler): + @swagger.operation(nickname='listAllVersions') + def get(self): + """ + @description: list all supported versions + @rtype: L{Versions} + """ + versions = [{'version': 'api.cvp.0.7.0', 'description': 'basics'}] + self.finish_request({'versions': versions}) diff --git a/opnfv_testapi/resources/models.py b/opnfv_testapi/resources/models.py new file mode 100644 index 0000000..e8fc532 --- /dev/null +++ b/opnfv_testapi/resources/models.py @@ -0,0 +1,115 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +# feng.xiaowei@zte.com.cn mv Pod to pod_models.py 5-18-2016 +# feng.xiaowei@zte.com.cn add MetaCreateResponse/MetaGetResponse 5-18-2016 +# feng.xiaowei@zte.com.cn mv TestProject to project_models.py 5-19-2016 +# feng.xiaowei@zte.com.cn delete meta class 5-19-2016 +# feng.xiaowei@zte.com.cn add CreateResponse 5-19-2016 +# feng.xiaowei@zte.com.cn mv TestCase to testcase_models.py 5-20-2016 +# feng.xiaowei@zte.com.cn mv TestResut to result_models.py 5-23-2016 +# feng.xiaowei@zte.com.cn add ModelBase 12-20-2016 +############################################################################## +import ast +import copy + +from opnfv_testapi.tornado_swagger import swagger + + +class ModelBase(object): + + def format(self): + return self._format(['_id']) + + def format_http(self): + return self._format([]) + + @classmethod + def from_dict(cls, a_dict): + if a_dict is None: + return None + + attr_parser = cls.attr_parser() + t = cls() + for k, v in a_dict.iteritems(): + value = v + if isinstance(v, dict) and k in attr_parser: + value = attr_parser[k].from_dict(v) + elif isinstance(v, list) and k in attr_parser: + value = [] + for item in v: + value.append(attr_parser[k].from_dict(item)) + + t.__setattr__(k, value) + + return t + + @staticmethod + def attr_parser(): + return {} + + def _format(self, excludes): + new_obj = copy.deepcopy(self) + dicts = new_obj.__dict__ + for k in dicts.keys(): + if k in excludes: + del dicts[k] + elif dicts[k]: + dicts[k] = self._obj_format(dicts[k]) + return dicts + + def _obj_format(self, obj): + if self._has_format(obj): + obj = obj.format() + elif isinstance(obj, unicode): + try: + obj = self._obj_format(ast.literal_eval(obj)) + except: + try: + obj = str(obj) + except: + obj = obj + elif isinstance(obj, list): + hs = list() + for h in obj: + hs.append(self._obj_format(h)) + obj = hs + elif not isinstance(obj, (str, int, float, dict)): + obj = str(obj) + return obj + + @staticmethod + def _has_format(obj): + return not isinstance(obj, (str, unicode)) and hasattr(obj, 'format') + + +@swagger.model() +class CreateResponse(ModelBase): + def __init__(self, href=''): + self.href = href + + +@swagger.model() +class Versions(ModelBase): + """ + @property versions: + @ptype versions: C{list} of L{Version} + """ + + def __init__(self): + self.versions = list() + + @staticmethod + def attr_parser(): + return {'versions': Version} + + +@swagger.model() +class Version(ModelBase): + def __init__(self, version=None, description=None): + self.version = version + self.description = description diff --git a/opnfv_testapi/resources/pod_handlers.py b/opnfv_testapi/resources/pod_handlers.py new file mode 100644 index 0000000..5029887 --- /dev/null +++ b/opnfv_testapi/resources/pod_handlers.py @@ -0,0 +1,78 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import handlers +from opnfv_testapi.resources import pod_models +from opnfv_testapi.tornado_swagger import swagger + + +class GenericPodHandler(handlers.GenericApiHandler): + def __init__(self, application, request, **kwargs): + super(GenericPodHandler, self).__init__(application, request, **kwargs) + self.table = 'pods' + self.table_cls = pod_models.Pod + + +class PodCLHandler(GenericPodHandler): + @swagger.operation(nickname='listAllPods') + def get(self): + """ + @description: list all pods + @return 200: list all pods, empty list is no pod exist + @rtype: L{Pods} + """ + self._list() + + @swagger.operation(nickname='createPod') + def post(self): + """ + @description: create a pod + @param body: pod to be created + @type body: L{PodCreateRequest} + @in body: body + @rtype: L{CreateResponse} + @return 200: pod is created. + @raise 403: pod already exists + @raise 400: body or name not provided + """ + def query(): + return {'name': self.json_args.get('name')} + miss_fields = ['name'] + self._create(miss_fields=miss_fields, query=query) + + +class PodGURHandler(GenericPodHandler): + @swagger.operation(nickname='getPodByName') + def get(self, pod_name): + """ + @description: get a single pod by pod_name + @rtype: L{Pod} + @return 200: pod exist + @raise 404: pod not exist + """ + self._get_one(query={'name': pod_name}) + + def delete(self, pod_name): + """ Remove a POD + + # check for an existing pod to be deleted + mongo_dict = yield self.db.pods.find_one( + {'name': pod_name}) + pod = TestProject.pod(mongo_dict) + if pod is None: + raise HTTPError(HTTP_NOT_FOUND, + "{} could not be found as a pod to be deleted" + .format(pod_name)) + + # just delete it, or maybe save it elsewhere in a future + res = yield self.db.projects.remove( + {'name': pod_name}) + + self.finish_request(answer) + """ + pass diff --git a/opnfv_testapi/resources/pod_models.py b/opnfv_testapi/resources/pod_models.py new file mode 100644 index 0000000..2c3ea97 --- /dev/null +++ b/opnfv_testapi/resources/pod_models.py @@ -0,0 +1,52 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from opnfv_testapi.resources import models +from opnfv_testapi.tornado_swagger import swagger + + +# name: name of the POD e.g. zte-1 +# mode: metal or virtual +# details: any detail +# role: ci-pod or community-pod or single-node + + +@swagger.model() +class PodCreateRequest(models.ModelBase): + def __init__(self, name, mode='', details='', role=""): + self.name = name + self.mode = mode + self.details = details + self.role = role + + +@swagger.model() +class Pod(models.ModelBase): + def __init__(self, + name='', mode='', details='', + role="", _id='', create_date=''): + self.name = name + self.mode = mode + self.details = details + self.role = role + self._id = _id + self.creation_date = create_date + + +@swagger.model() +class Pods(models.ModelBase): + """ + @property pods: + @ptype pods: C{list} of L{Pod} + """ + def __init__(self): + self.pods = list() + + @staticmethod + def attr_parser(): + return {'pods': Pod} diff --git a/opnfv_testapi/resources/project_handlers.py b/opnfv_testapi/resources/project_handlers.py new file mode 100644 index 0000000..be29507 --- /dev/null +++ b/opnfv_testapi/resources/project_handlers.py @@ -0,0 +1,86 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +from opnfv_testapi.resources import handlers +from opnfv_testapi.resources import project_models +from opnfv_testapi.tornado_swagger import swagger + + +class GenericProjectHandler(handlers.GenericApiHandler): + def __init__(self, application, request, **kwargs): + super(GenericProjectHandler, self).__init__(application, + request, + **kwargs) + self.table = 'projects' + self.table_cls = project_models.Project + + +class ProjectCLHandler(GenericProjectHandler): + @swagger.operation(nickname="listAllProjects") + def get(self): + """ + @description: list all projects + @return 200: return all projects, empty list is no project exist + @rtype: L{Projects} + """ + self._list() + + @swagger.operation(nickname="createProject") + def post(self): + """ + @description: create a project + @param body: project to be created + @type body: L{ProjectCreateRequest} + @in body: body + @rtype: L{CreateResponse} + @return 200: project is created. + @raise 403: project already exists + @raise 400: body or name not provided + """ + def query(): + return {'name': self.json_args.get('name')} + miss_fields = ['name'] + self._create(miss_fields=miss_fields, query=query) + + +class ProjectGURHandler(GenericProjectHandler): + @swagger.operation(nickname='getProjectByName') + def get(self, project_name): + """ + @description: get a single project by project_name + @rtype: L{Project} + @return 200: project exist + @raise 404: project not exist + """ + self._get_one(query={'name': project_name}) + + @swagger.operation(nickname="updateProjectByName") + def put(self, project_name): + """ + @description: update a single project by project_name + @param body: project to be updated + @type body: L{ProjectUpdateRequest} + @in body: body + @rtype: L{Project} + @return 200: update success + @raise 404: project not exist + @raise 403: new project name already exist or nothing to update + """ + query = {'name': project_name} + db_keys = ['name'] + self._update(query=query, db_keys=db_keys) + + @swagger.operation(nickname='deleteProjectByName') + def delete(self, project_name): + """ + @description: delete a project by project_name + @return 200: delete success + @raise 404: project not exist + """ + self._delete(query={'name': project_name}) diff --git a/opnfv_testapi/resources/project_models.py b/opnfv_testapi/resources/project_models.py new file mode 100644 index 0000000..3243882 --- /dev/null +++ b/opnfv_testapi/resources/project_models.py @@ -0,0 +1,48 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from opnfv_testapi.resources import models +from opnfv_testapi.tornado_swagger import swagger + + +@swagger.model() +class ProjectCreateRequest(models.ModelBase): + def __init__(self, name, description=''): + self.name = name + self.description = description + + +@swagger.model() +class ProjectUpdateRequest(models.ModelBase): + def __init__(self, name='', description=''): + self.name = name + self.description = description + + +@swagger.model() +class Project(models.ModelBase): + def __init__(self, + name=None, _id=None, description=None, create_date=None): + self._id = _id + self.name = name + self.description = description + self.creation_date = create_date + + +@swagger.model() +class Projects(models.ModelBase): + """ + @property projects: + @ptype projects: C{list} of L{Project} + """ + def __init__(self): + self.projects = list() + + @staticmethod + def attr_parser(): + return {'projects': Project} diff --git a/opnfv_testapi/resources/result_handlers.py b/opnfv_testapi/resources/result_handlers.py new file mode 100644 index 0000000..2e65ba4 --- /dev/null +++ b/opnfv_testapi/resources/result_handlers.py @@ -0,0 +1,308 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import logging +from datetime import datetime +from datetime import timedelta +import json +import tarfile +import io + +from tornado import gen +from tornado import web +from bson import objectid + +from opnfv_testapi.common.config import CONF +from opnfv_testapi.common import message +from opnfv_testapi.common import raises +from opnfv_testapi.resources import handlers +from opnfv_testapi.resources import result_models +from opnfv_testapi.tornado_swagger import swagger +from opnfv_testapi.ui.auth import constants as auth_const + + +class GenericResultHandler(handlers.GenericApiHandler): + def __init__(self, application, request, **kwargs): + super(GenericResultHandler, self).__init__(application, + request, + **kwargs) + self.table = self.db_results + self.table_cls = result_models.TestResult + + def get_int(self, key, value): + try: + value = int(value) + except: + raises.BadRequest(message.must_int(key)) + return value + + def set_query(self): + query = dict() + date_range = dict() + + query['public'] = {'$not': {'$eq': 'false'}} + for k in self.request.query_arguments.keys(): + v = self.get_query_argument(k) + if k == 'project' or k == 'pod' or k == 'case': + query[k + '_name'] = v + elif k == 'period': + v = self.get_int(k, v) + if v > 0: + period = datetime.now() - timedelta(days=v) + obj = {"$gte": str(period)} + query['start_date'] = obj + elif k == 'trust_indicator': + query[k + '.current'] = float(v) + elif k == 'from': + date_range.update({'$gte': str(v)}) + elif k == 'to': + date_range.update({'$lt': str(v)}) + elif k == 'signed': + openid = self.get_secure_cookie(auth_const.OPENID) + role = self.get_secure_cookie(auth_const.ROLE) + logging.info('role:%s', role) + if role: + del query['public'] + query['user'] = openid + if role.find("reviewer") != -1: + del query['user'] + query['review'] = 'true' + elif k not in ['last', 'page', 'descend']: + query[k] = v + if date_range: + query['start_date'] = date_range + + # if $lt is not provided, + # empty/None/null/'' start_date will also be returned + if 'start_date' in query and '$lt' not in query['start_date']: + query['start_date'].update({'$lt': str(datetime.now())}) + + return query + + +class ResultsCLHandler(GenericResultHandler): + @swagger.operation(nickname="queryTestResults") + def get(self): + """ + @description: Retrieve result(s) for a test project + on a specific pod. + @notes: Retrieve result(s) for a test project on a specific pod. + Available filters for this request are : + - project : project name + - case : case name + - pod : pod name + - version : platform version (Arno-R1, ...) + - installer : fuel/apex/compass/joid/daisy + - build_tag : Jenkins build tag name + - period : x last days, incompatible with from/to + - from : starting time in 2016-01-01 or 2016-01-01 00:01:23 + - to : ending time in 2016-01-01 or 2016-01-01 00:01:23 + - scenario : the test scenario (previously version) + - criteria : the global criteria status passed or failed + - trust_indicator : evaluate the stability of the test case + to avoid running systematically long and stable test case + - signed : get logined user result + + GET /results/project=functest&case=vPing&version=Arno-R1 \ + &pod=pod_name&period=15&signed + @return 200: all test results consist with query, + empty list if no result is found + @rtype: L{TestResults} + @param pod: pod name + @type pod: L{string} + @in pod: query + @required pod: False + @param project: project name + @type project: L{string} + @in project: query + @required project: False + @param case: case name + @type case: L{string} + @in case: query + @required case: False + @param version: i.e. Colorado + @type version: L{string} + @in version: query + @required version: False + @param installer: fuel/apex/joid/compass + @type installer: L{string} + @in installer: query + @required installer: False + @param build_tag: i.e. v3.0 + @type build_tag: L{string} + @in build_tag: query + @required build_tag: False + @param scenario: i.e. odl + @type scenario: L{string} + @in scenario: query + @required scenario: False + @param criteria: i.e. passed + @type criteria: L{string} + @in criteria: query + @required criteria: False + @param period: last days + @type period: L{string} + @in period: query + @required period: False + @param from: i.e. 2016-01-01 or 2016-01-01 00:01:23 + @type from: L{string} + @in from: query + @required from: False + @param to: i.e. 2016-01-01 or 2016-01-01 00:01:23 + @type to: L{string} + @in to: query + @required to: False + @param last: last records stored until now + @type last: L{string} + @in last: query + @required last: False + @param page: which page to list + @type page: L{int} + @in page: query + @required page: False + @param trust_indicator: must be float + @type trust_indicator: L{float} + @in trust_indicator: query + @required trust_indicator: False + @param signed: user results or all results + @type signed: L{string} + @in signed: query + @required signed: False + @param descend: true, newest2oldest; false, oldest2newest + @type descend: L{string} + @in descend: query + @required descend: False + """ + def descend_limit(): + descend = self.get_query_argument('descend', 'true') + return -1 if descend.lower() == 'true' else 1 + + def last_limit(): + return self.get_int('last', self.get_query_argument('last', 0)) + + def page_limit(): + return self.get_int('page', self.get_query_argument('page', 0)) + + limitations = { + 'sort': {'_id': descend_limit()}, + 'last': last_limit(), + 'page': page_limit(), + 'per_page': CONF.api_results_per_page + } + + self._list(query=self.set_query(), **limitations) + + @swagger.operation(nickname="createTestResult") + def post(self): + """ + @description: create a test result + @param body: result to be created + @type body: L{ResultCreateRequest} + @in body: body + @rtype: L{CreateResponse} + @return 200: result is created. + @raise 404: pod/project/testcase not exist + @raise 400: body/pod_name/project_name/case_name not provided + """ + self._post() + + def _post(self): + def pod_query(): + return {'name': self.json_args.get('pod_name')} + + def project_query(): + return {'name': self.json_args.get('project_name')} + + def testcase_query(): + return {'project_name': self.json_args.get('project_name'), + 'name': self.json_args.get('case_name')} + + miss_fields = ['pod_name', 'project_name', 'case_name'] + carriers = [('pods', pod_query), + ('projects', project_query), + ('testcases', testcase_query)] + + self._create(miss_fields=miss_fields, carriers=carriers) + + +class ResultsUploadHandler(ResultsCLHandler): + @swagger.operation(nickname="uploadTestResult") + @web.asynchronous + @gen.coroutine + def post(self): + """ + @description: upload and create a test result + @param body: result to be created + @type body: L{ResultCreateRequest} + @in body: body + @rtype: L{CreateResponse} + @return 200: result is created. + @raise 404: pod/project/testcase not exist + @raise 400: body/pod_name/project_name/case_name not provided + """ + fileinfo = self.request.files['file'][0] + tar_in = tarfile.open(fileobj=io.BytesIO(fileinfo['body']), + mode="r:gz") + try: + results = tar_in.extractfile('results/results.json').read() + except KeyError: + msg = 'Uploaded results must contain at least one passing test.' + self.finish_request({'code': 403, 'msg': msg}) + return + results = results.split('\n') + result_ids = [] + for result in results: + if result == '': + continue + self.json_args = json.loads(result).copy() + build_tag = self.json_args['build_tag'] + _id = yield self._inner_create() + result_ids.append(str(_id)) + test_id = build_tag[13:49] + log_path = '/home/testapi/logs/%s' % (test_id) + tar_in.extractall(log_path) + log_filename = "/home/testapi/logs/log_%s.tar.gz" % (test_id) + with open(log_filename, "wb") as tar_out: + tar_out.write(fileinfo['body']) + resp = {'id': test_id, 'results': result_ids} + self.finish_request(resp) + + +class ResultsGURHandler(GenericResultHandler): + @swagger.operation(nickname='DeleteTestResultById') + def delete(self, result_id): + query = {'_id': objectid.ObjectId(result_id)} + self._delete(query=query) + + @swagger.operation(nickname='getTestResultById') + def get(self, result_id): + """ + @description: get a single result by result_id + @rtype: L{TestResult} + @return 200: test result exist + @raise 404: test result not exist + """ + query = dict() + query["_id"] = objectid.ObjectId(result_id) + self._get_one(query=query) + + @swagger.operation(nickname="updateTestResultById") + def put(self, result_id): + """ + @description: update a single result by _id + @param body: fields to be updated + @type body: L{ResultUpdateRequest} + @in body: body + @rtype: L{Result} + @return 200: update success + @raise 404: result not exist + @raise 403: nothing to update + """ + query = {'_id': objectid.ObjectId(result_id)} + db_keys = [] + self._update(query=query, db_keys=db_keys) diff --git a/opnfv_testapi/resources/result_models.py b/opnfv_testapi/resources/result_models.py new file mode 100644 index 0000000..698f498 --- /dev/null +++ b/opnfv_testapi/resources/result_models.py @@ -0,0 +1,133 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from opnfv_testapi.resources import models +from opnfv_testapi.tornado_swagger import swagger + + +@swagger.model() +class TIHistory(models.ModelBase): + """ + @ptype step: L{float} + """ + def __init__(self, date=None, step=0): + self.date = date + self.step = step + + +@swagger.model() +class TI(models.ModelBase): + """ + @property histories: trust_indicator update histories + @ptype histories: C{list} of L{TIHistory} + @ptype current: L{float} + """ + def __init__(self, current=0): + self.current = current + self.histories = list() + + @staticmethod + def attr_parser(): + return {'histories': TIHistory} + + +@swagger.model() +class ResultCreateRequest(models.ModelBase): + """ + @property trust_indicator: + @ptype trust_indicator: L{TI} + """ + def __init__(self, + pod_name=None, + project_name=None, + case_name=None, + installer=None, + version=None, + start_date=None, + stop_date=None, + details=None, + build_tag=None, + scenario=None, + criteria=None, + user=None, + public="true", + review="false", + trust_indicator=None): + self.pod_name = pod_name + self.project_name = project_name + self.case_name = case_name + self.installer = installer + self.version = version + self.start_date = start_date + self.stop_date = stop_date + self.details = details + self.build_tag = build_tag + self.scenario = scenario + self.criteria = criteria + self.user = user + self.public = public + self.review = review + self.trust_indicator = trust_indicator if trust_indicator else TI(0) + + +@swagger.model() +class ResultUpdateRequest(models.ModelBase): + """ + @property trust_indicator: + @ptype trust_indicator: L{TI} + """ + def __init__(self, trust_indicator=None): + self.trust_indicator = trust_indicator + + +@swagger.model() +class TestResult(models.ModelBase): + """ + @property trust_indicator: used for long duration test case + @ptype trust_indicator: L{TI} + """ + def __init__(self, _id=None, case_name=None, project_name=None, + pod_name=None, installer=None, version=None, + start_date=None, stop_date=None, details=None, + build_tag=None, scenario=None, criteria=None, + user=None, public="true", review="false", + trust_indicator=None): + self._id = _id + self.case_name = case_name + self.project_name = project_name + self.pod_name = pod_name + self.installer = installer + self.version = version + self.start_date = start_date + self.stop_date = stop_date + self.details = details + self.build_tag = build_tag + self.scenario = scenario + self.criteria = criteria + self.user = user + self.public = public + self.review = review + self.trust_indicator = trust_indicator + + @staticmethod + def attr_parser(): + return {'trust_indicator': TI} + + +@swagger.model() +class TestResults(models.ModelBase): + """ + @property results: + @ptype results: C{list} of L{TestResult} + """ + def __init__(self): + self.results = list() + + @staticmethod + def attr_parser(): + return {'results': TestResult} diff --git a/opnfv_testapi/resources/scenario_handlers.py b/opnfv_testapi/resources/scenario_handlers.py new file mode 100644 index 0000000..5d420a5 --- /dev/null +++ b/opnfv_testapi/resources/scenario_handlers.py @@ -0,0 +1,282 @@ +import functools + +from opnfv_testapi.common import message +from opnfv_testapi.common import raises +from opnfv_testapi.resources import handlers +import opnfv_testapi.resources.scenario_models as models +from opnfv_testapi.tornado_swagger import swagger + + +class GenericScenarioHandler(handlers.GenericApiHandler): + def __init__(self, application, request, **kwargs): + super(GenericScenarioHandler, self).__init__(application, + request, + **kwargs) + self.table = self.db_scenarios + self.table_cls = models.Scenario + + +class ScenariosCLHandler(GenericScenarioHandler): + @swagger.operation(nickname="queryScenarios") + def get(self): + """ + @description: Retrieve scenario(s). + @notes: Retrieve scenario(s) + Available filters for this request are : + - name : scenario name + + GET /scenarios?name=scenario_1 + @param name: scenario name + @type name: L{string} + @in name: query + @required name: False + @param installer: installer type + @type installer: L{string} + @in installer: query + @required installer: False + @param version: version + @type version: L{string} + @in version: query + @required version: False + @param project: project name + @type project: L{string} + @in project: query + @required project: False + @return 200: all scenarios satisfy queries, + empty list if no scenario is found + @rtype: L{Scenarios} + """ + + def _set_query(): + query = dict() + elem_query = dict() + for k in self.request.query_arguments.keys(): + v = self.get_query_argument(k) + if k == 'installer': + elem_query["installer"] = v + elif k == 'version': + elem_query["versions.version"] = v + elif k == 'project': + elem_query["versions.projects.project"] = v + else: + query[k] = v + if elem_query: + query['installers'] = {'$elemMatch': elem_query} + return query + + self._list(query=_set_query()) + + @swagger.operation(nickname="createScenario") + def post(self): + """ + @description: create a new scenario by name + @param body: scenario to be created + @type body: L{ScenarioCreateRequest} + @in body: body + @rtype: L{CreateResponse} + @return 200: scenario is created. + @raise 403: scenario already exists + @raise 400: body or name not provided + """ + def query(): + return {'name': self.json_args.get('name')} + miss_fields = ['name'] + self._create(miss_fields=miss_fields, query=query) + + +class ScenarioGURHandler(GenericScenarioHandler): + @swagger.operation(nickname='getScenarioByName') + def get(self, name): + """ + @description: get a single scenario by name + @rtype: L{Scenario} + @return 200: scenario exist + @raise 404: scenario not exist + """ + self._get_one(query={'name': name}) + pass + + @swagger.operation(nickname="updateScenarioByName") + def put(self, name): + """ + @description: update a single scenario by name + @param body: fields to be updated + @type body: L{ScenarioUpdateRequest} + @in body: body + @rtype: L{Scenario} + @return 200: update success + @raise 404: scenario not exist + @raise 403: nothing to update + """ + query = {'name': name} + db_keys = ['name'] + self._update(query=query, db_keys=db_keys) + + @swagger.operation(nickname="deleteScenarioByName") + def delete(self, name): + """ + @description: delete a scenario by name + @return 200: delete success + @raise 404: scenario not exist: + """ + + self._delete(query={'name': name}) + + def _update_query(self, keys, data): + query = dict() + if self._is_rename(): + new = self._term.get('name') + if data.get('name') != new: + query['name'] = new + + return query + + def _update_requests(self, data): + updates = { + ('name', 'update'): self._update_requests_rename, + ('installer', 'add'): self._update_requests_add_installer, + ('installer', 'delete'): self._update_requests_delete_installer, + ('version', 'add'): self._update_requests_add_version, + ('version', 'delete'): self._update_requests_delete_version, + ('owner', 'update'): self._update_requests_change_owner, + ('project', 'add'): self._update_requests_add_project, + ('project', 'delete'): self._update_requests_delete_project, + ('customs', 'add'): self._update_requests_add_customs, + ('customs', 'delete'): self._update_requests_delete_customs, + ('score', 'add'): self._update_requests_add_score, + ('trust_indicator', 'add'): self._update_requests_add_ti, + } + + updates[(self._field, self._op)](data) + + return data.format() + + def _iter_installers(xstep): + @functools.wraps(xstep) + def magic(self, data): + [xstep(self, installer) + for installer in self._filter_installers(data.installers)] + return magic + + def _iter_versions(xstep): + @functools.wraps(xstep) + def magic(self, installer): + [xstep(self, version) + for version in (self._filter_versions(installer.versions))] + return magic + + def _iter_projects(xstep): + @functools.wraps(xstep) + def magic(self, version): + [xstep(self, project) + for project in (self._filter_projects(version.projects))] + return magic + + def _update_requests_rename(self, data): + data.name = self._term.get('name') + if not data.name: + raises.BadRequest(message.missing('name')) + + def _update_requests_add_installer(self, data): + data.installers.append(models.ScenarioInstaller.from_dict(self._term)) + + def _update_requests_delete_installer(self, data): + data.installers = self._remove_installers(data.installers) + + @_iter_installers + def _update_requests_add_version(self, installer): + installer.versions.append(models.ScenarioVersion.from_dict(self._term)) + + @_iter_installers + def _update_requests_delete_version(self, installer): + installer.versions = self._remove_versions(installer.versions) + + @_iter_installers + @_iter_versions + def _update_requests_change_owner(self, version): + version.owner = self._term.get('owner') + + @_iter_installers + @_iter_versions + def _update_requests_add_project(self, version): + version.projects.append(models.ScenarioProject.from_dict(self._term)) + + @_iter_installers + @_iter_versions + def _update_requests_delete_project(self, version): + version.projects = self._remove_projects(version.projects) + + @_iter_installers + @_iter_versions + @_iter_projects + def _update_requests_add_customs(self, project): + project.customs = list(set(project.customs + self._term)) + + @_iter_installers + @_iter_versions + @_iter_projects + def _update_requests_delete_customs(self, project): + project.customs = filter( + lambda f: f not in self._term, + project.customs) + + @_iter_installers + @_iter_versions + @_iter_projects + def _update_requests_add_score(self, project): + project.scores.append( + models.ScenarioScore.from_dict(self._term)) + + @_iter_installers + @_iter_versions + @_iter_projects + def _update_requests_add_ti(self, project): + project.trust_indicators.append( + models.ScenarioTI.from_dict(self._term)) + + def _is_rename(self): + return self._field == 'name' and self._op == 'update' + + def _remove_installers(self, installers): + return self._remove('installer', installers) + + def _filter_installers(self, installers): + return self._filter('installer', installers) + + def _remove_versions(self, versions): + return self._remove('version', versions) + + def _filter_versions(self, versions): + return self._filter('version', versions) + + def _remove_projects(self, projects): + return self._remove('project', projects) + + def _filter_projects(self, projects): + return self._filter('project', projects) + + def _remove(self, field, fields): + return filter( + lambda f: getattr(f, field) != self._locate.get(field), + fields) + + def _filter(self, field, fields): + return filter( + lambda f: getattr(f, field) == self._locate.get(field), + fields) + + @property + def _field(self): + return self.json_args.get('field') + + @property + def _op(self): + return self.json_args.get('op') + + @property + def _locate(self): + return self.json_args.get('locate') + + @property + def _term(self): + return self.json_args.get('term') diff --git a/opnfv_testapi/resources/scenario_models.py b/opnfv_testapi/resources/scenario_models.py new file mode 100644 index 0000000..467cff2 --- /dev/null +++ b/opnfv_testapi/resources/scenario_models.py @@ -0,0 +1,204 @@ +from opnfv_testapi.resources import models +from opnfv_testapi.tornado_swagger import swagger + + +def list_default(value): + return value if value else list() + + +def dict_default(value): + return value if value else dict() + + +@swagger.model() +class ScenarioTI(models.ModelBase): + def __init__(self, date=None, status='silver'): + self.date = date + self.status = status + + +@swagger.model() +class ScenarioScore(models.ModelBase): + def __init__(self, date=None, score='0'): + self.date = date + self.score = score + + +@swagger.model() +class ScenarioProject(models.ModelBase): + """ + @property customs: + @ptype customs: C{list} of L{string} + @property scores: + @ptype scores: C{list} of L{ScenarioScore} + @property trust_indicators: + @ptype trust_indicators: C{list} of L{ScenarioTI} + """ + def __init__(self, + project='', + customs=None, + scores=None, + trust_indicators=None): + self.project = project + self.customs = list_default(customs) + self.scores = list_default(scores) + self.trust_indicators = list_default(trust_indicators) + + @staticmethod + def attr_parser(): + return {'scores': ScenarioScore, + 'trust_indicators': ScenarioTI} + + def __eq__(self, other): + return [self.project == other.project and + self._customs_eq(other) and + self._scores_eq(other) and + self._ti_eq(other)] + + def __ne__(self, other): + return not self.__eq__(other) + + def _customs_eq(self, other): + return set(self.customs) == set(other.customs) + + def _scores_eq(self, other): + return set(self.scores) == set(other.scores) + + def _ti_eq(self, other): + return set(self.trust_indicators) == set(other.trust_indicators) + + +@swagger.model() +class ScenarioVersion(models.ModelBase): + """ + @property projects: + @ptype projects: C{list} of L{ScenarioProject} + """ + def __init__(self, version=None, projects=None): + self.version = version + self.projects = list_default(projects) + + @staticmethod + def attr_parser(): + return {'projects': ScenarioProject} + + def __eq__(self, other): + return [self.version == other.version and self._projects_eq(other)] + + def __ne__(self, other): + return not self.__eq__(other) + + def _projects_eq(self, other): + for s_project in self.projects: + for o_project in other.projects: + if s_project.project == o_project.project: + if s_project != o_project: + return False + + return True + + +@swagger.model() +class ScenarioInstaller(models.ModelBase): + """ + @property versions: + @ptype versions: C{list} of L{ScenarioVersion} + """ + def __init__(self, installer=None, versions=None): + self.installer = installer + self.versions = list_default(versions) + + @staticmethod + def attr_parser(): + return {'versions': ScenarioVersion} + + def __eq__(self, other): + return [self.installer == other.installer and self._versions_eq(other)] + + def __ne__(self, other): + return not self.__eq__(other) + + def _versions_eq(self, other): + for s_version in self.versions: + for o_version in other.versions: + if s_version.version == o_version.version: + if s_version != o_version: + return False + + return True + + +@swagger.model() +class ScenarioCreateRequest(models.ModelBase): + """ + @property installers: + @ptype installers: C{list} of L{ScenarioInstaller} + """ + def __init__(self, name='', installers=None): + self.name = name + self.installers = list_default(installers) + + @staticmethod + def attr_parser(): + return {'installers': ScenarioInstaller} + + +@swagger.model() +class ScenarioUpdateRequest(models.ModelBase): + """ + @property field: update field + @property op: add/delete/update + @property locate: information used to locate the field + @property term: new value + """ + def __init__(self, field=None, op=None, locate=None, term=None): + self.field = field + self.op = op + self.locate = dict_default(locate) + self.term = dict_default(term) + + +@swagger.model() +class Scenario(models.ModelBase): + """ + @property installers: + @ptype installers: C{list} of L{ScenarioInstaller} + """ + def __init__(self, name='', create_date='', _id='', installers=None): + self.name = name + self._id = _id + self.creation_date = create_date + self.installers = list_default(installers) + + @staticmethod + def attr_parser(): + return {'installers': ScenarioInstaller} + + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + return [self.name == other.name and self._installers_eq(other)] + + def _installers_eq(self, other): + for s_install in self.installers: + for o_install in other.installers: + if s_install.installer == o_install.installer: + if s_install != o_install: + return False + + return True + + +@swagger.model() +class Scenarios(models.ModelBase): + """ + @property scenarios: + @ptype scenarios: C{list} of L{Scenario} + """ + def __init__(self): + self.scenarios = list() + + @staticmethod + def attr_parser(): + return {'scenarios': Scenario} diff --git a/opnfv_testapi/resources/sut_handlers.py b/opnfv_testapi/resources/sut_handlers.py new file mode 100644 index 0000000..16c50b8 --- /dev/null +++ b/opnfv_testapi/resources/sut_handlers.py @@ -0,0 +1,112 @@ +############################################################################## +# Copyright (c) 2017 +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import logging +import json +import os + +from opnfv_testapi.resources import handlers +from opnfv_testapi.resources import sut_models +from opnfv_testapi.tornado_swagger import swagger + +LOG = logging.getLogger(__name__) +LOG.setLevel(logging.DEBUG) + +RESULT_PATH = '/home/testapi/logs/{}/results' + + +class GenericSutHandler(handlers.GenericApiHandler): + def __init__(self, application, request, **kwargs): + super(GenericSutHandler, self).__init__(application, + request, + **kwargs) + self.table = "suts" + self.table_cls = sut_models.Sut + + +class HardwareHandler(GenericSutHandler): + @swagger.operation(nickname="getHardwareById") + def get(self, id): + endpoint_info = self._read_endpoint_info(id) + LOG.debug('Endpoint info: %s', endpoint_info) + + all_info = self._read_sut_info(id) + LOG.debug('All SUT info: %s', all_info) + + hardware_info = {k: self._get_single_host_info(v) + for k, v in all_info.items()} + LOG.debug('SUT info: %s', hardware_info) + + data = { + 'endpoint_info': endpoint_info, + 'hardware_info': hardware_info + } + + self.write(data) + + def _read_endpoint_info(self, id): + path = os.path.join(RESULT_PATH.format(id), 'endpoint_info.json') + try: + with open(path) as f: + endpoint_info = json.load(f) + except Exception: + endpoint_info = [] + + return endpoint_info + + def _read_sut_info(self, id): + path = os.path.join(RESULT_PATH.format(id), 'all_hosts_info.json') + try: + with open(path) as f: + all_info = json.load(f) + except Exception: + all_info = {} + return all_info + + def _get_single_host_info(self, single_info): + info = [] + facts = single_info.get('ansible_facts', {}) + + info.append(['hostname', facts.get('ansible_hostname')]) + + info.append(['product_name', facts.get('ansible_product_name')]) + info.append(['product_version', facts.get('ansible_product_version')]) + + processors = facts.get('ansible_processor', []) + try: + processor_type = '{} {}'.format(processors[0], processors[1]) + except IndexError: + LOG.exception('No Processor in SUT data') + processor_type = None + info.append(['processor_type', processor_type]) + info.append(['architecture', facts.get('ansible_architecture')]) + info.append(['processor_cores', facts.get('ansible_processor_cores')]) + info.append(['processor_vcpus', facts.get('ansible_processor_vcpus')]) + + memory = facts.get('ansible_memtotal_mb') + memory = round(memory * 1.0 / 1024, 2) if memory else None + info.append(['memory', '{} GB'.format(memory)]) + + devices = facts.get('ansible_devices', {}) + info.extend([self._get_device_info(k, v) for k, v in devices.items()]) + + lsb_description = facts.get('ansible_lsb', {}).get('description') + info.append(['OS', lsb_description]) + + interfaces = facts.get('ansible_interfaces') + info.append(['interfaces', interfaces]) + info.extend([self._get_interface_info(facts, i) for i in interfaces]) + info = [i for i in info if i] + + return info + + def _get_interface_info(self, facts, name): + mac = facts.get('ansible_{}'.format(name), {}).get('macaddress') + return [name, mac] if mac else [] + + def _get_device_info(self, name, info): + return ['disk_{}'.format(name), info.get('size')] diff --git a/opnfv_testapi/resources/sut_models.py b/opnfv_testapi/resources/sut_models.py new file mode 100644 index 0000000..b4a869b --- /dev/null +++ b/opnfv_testapi/resources/sut_models.py @@ -0,0 +1,31 @@ +############################################################################## +# Copyright (c) 2017 +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from opnfv_testapi.resources import models +from opnfv_testapi.tornado_swagger import swagger + + +@swagger.model() +class Sut(models.ModelBase): + """ + """ + def __init__(self): + pass + + +@swagger.model() +class Suts(models.ModelBase): + """ + @property suts: + @ptype tests: C{list} of L{Sut} + """ + def __init__(self): + self.suts = list() + + @staticmethod + def attr_parser(): + return {'suts': Sut} diff --git a/opnfv_testapi/resources/test_handlers.py b/opnfv_testapi/resources/test_handlers.py new file mode 100644 index 0000000..82cf9ae --- /dev/null +++ b/opnfv_testapi/resources/test_handlers.py @@ -0,0 +1,307 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import logging +import os +import json + +from tornado import web +from tornado import gen +from bson import objectid + +from opnfv_testapi.common.config import CONF +from opnfv_testapi.common import message +from opnfv_testapi.common import raises +from opnfv_testapi.resources import handlers +from opnfv_testapi.resources import test_models +from opnfv_testapi.tornado_swagger import swagger +from opnfv_testapi.ui.auth import constants as auth_const +from opnfv_testapi.db import api as dbapi + +DOVETAIL_LOG_PATH = '/home/testapi/logs/{}/results/dovetail.log' + + +class GenericTestHandler(handlers.GenericApiHandler): + def __init__(self, application, request, **kwargs): + super(GenericTestHandler, self).__init__(application, + request, + **kwargs) + self.table = "tests" + self.table_cls = test_models.Test + + +class TestsCLHandler(GenericTestHandler): + @swagger.operation(nickname="queryTests") + @web.asynchronous + @gen.coroutine + def get(self): + """ + @description: Retrieve result(s) for a test project + on a specific pod. + @notes: Retrieve result(s) for a test project on a specific pod. + Available filters for this request are : + - id : Test id + - period : x last days, incompatible with from/to + - from : starting time in 2016-01-01 or 2016-01-01 00:01:23 + - to : ending time in 2016-01-01 or 2016-01-01 00:01:23 + - signed : get logined user result + + GET /results/project=functest&case=vPing&version=Arno-R1 \ + &pod=pod_name&period=15&signed + @return 200: all test results consist with query, + empty list if no result is found + @rtype: L{Tests} + """ + def descend_limit(): + descend = self.get_query_argument('descend', 'true') + return -1 if descend.lower() == 'true' else 1 + + def last_limit(): + return self.get_int('last', self.get_query_argument('last', 0)) + + def page_limit(): + return self.get_int('page', self.get_query_argument('page', 0)) + + limitations = { + 'sort': {'_id': descend_limit()}, + 'last': last_limit(), + 'page': page_limit(), + 'per_page': CONF.api_results_per_page + } + + query = yield self.set_query() + yield self._list(query=query, **limitations) + logging.debug('list end') + + @swagger.operation(nickname="createTest") + @web.asynchronous + def post(self): + """ + @description: create a test + @param body: test to be created + @type body: L{TestCreateRequest} + @in body: body + @rtype: L{CreateResponse} + @return 200: test is created. + @raise 404: pod/project/testcase not exist + @raise 400: body/pod_name/project_name/case_name not provided + """ + openid = self.get_secure_cookie(auth_const.OPENID) + if openid: + self.json_args['owner'] = openid + + self._post() + + @gen.coroutine + def _post(self): + miss_fields = [] + carriers = [] + query = {'owner': self.json_args['owner'], 'id': self.json_args['id']} + ret, msg = yield self._check_if_exists(table="tests", query=query) + if ret: + self.finish_request({'code': '403', 'msg': msg}) + return + + self._create(miss_fields=miss_fields, carriers=carriers) + + +class TestsGURHandler(GenericTestHandler): + + @swagger.operation(nickname="getTestById") + @web.asynchronous + @gen.coroutine + def get(self, test_id): + query = dict() + query["_id"] = objectid.ObjectId(test_id) + + data = yield dbapi.db_find_one(self.table, query) + if not data: + raises.NotFound(message.not_found(self.table, query)) + + validation = yield self._check_api_response_validation(data['id']) + + data.update({'validation': validation}) + + self.finish_request(self.format_data(data)) + + @gen.coroutine + def _check_api_response_validation(self, test_id): + log_path = DOVETAIL_LOG_PATH.format(test_id) + if not os.path.exists(log_path): + raises.Forbidden('dovetail.log not found, please check') + + with open(log_path) as f: + log_content = f.read() + + warning_keyword = 'Strict API response validation DISABLED' + if warning_keyword in log_content: + raise gen.Return('API response validation disabled') + else: + raise gen.Return('API response validation enabled') + + @swagger.operation(nickname="deleteTestById") + def delete(self, test_id): + query = {'_id': objectid.ObjectId(test_id)} + self._delete(query=query) + + @swagger.operation(nickname="updateTestById") + @web.asynchronous + def put(self, _id): + """ + @description: update a single test by id + @param body: fields to be updated + @type body: L{TestUpdateRequest} + @in body: body + @rtype: L{Test} + @return 200: update success + @raise 404: Test not exist + @raise 403: nothing to update + """ + logging.debug('put') + data = json.loads(self.request.body) + item = data.get('item') + value = data.get(item) + logging.debug('%s:%s', item, value) + try: + self.update(_id, item, value) + except Exception as e: + logging.error('except:%s', e) + return + + @gen.coroutine + def _convert_to_id(self, email): + query = {"email": email} + table = "users" + if query and table: + data = yield dbapi.db_find_one(table, query) + if data: + raise gen.Return((True, 'Data alreay exists. %s' % (query), + data.get("openid"))) + raise gen.Return((False, 'Data does not exist. %s' % (query), None)) + + @gen.coroutine + def update(self, _id, item, value): + logging.debug("update") + if item == "shared": + new_list = [] + for user in value: + ret, msg, user_id = yield self._convert_to_id(user) + if ret: + user = user_id + new_list.append(user) + query = {"$or": [{"openid": user}, {"email": user}]} + table = "users" + ret, msg = yield self._check_if_exists(table=table, + query=query) + logging.debug('ret:%s', ret) + if not ret: + self.finish_request({'code': '403', 'msg': msg}) + return + + if len(new_list) != len(set(new_list)): + msg = "Already shared with this user" + self.finish_request({'code': '403', 'msg': msg}) + return + + logging.debug("before _update") + self.json_args = {} + self.json_args[item] = value + ret, msg = yield self.check_auth(item, value) + if not ret: + self.finish_request({'code': '404', 'msg': msg}) + return + + query = {'_id': objectid.ObjectId(_id)} + db_keys = ['_id', ] + + test = yield dbapi.db_find_one("tests", query) + if not test: + msg = 'Record does not exist' + self.finish_request({'code': 404, 'msg': msg}) + return + + curr_user = self.get_secure_cookie(auth_const.OPENID) + if item in {"shared", "label", "sut_label"}: + query['owner'] = curr_user + db_keys.append('owner') + + if item == 'sut_label': + if test['status'] != 'private' and not value: + msg = 'SUT version cannot be changed to None after submitting.' + self.finish_request({'code': 403, 'msg': msg}) + return + + if item == "status": + if value in {'approved', 'not approved'}: + if test['status'] == 'private': + msg = 'Not allowed to approve/not approve' + self.finish_request({'code': 403, 'msg': msg}) + return + + user = yield dbapi.db_find_one("users", {'openid': curr_user}) + if 'administrator' not in user['role']: + msg = 'No permission to operate' + self.finish_request({'code': 403, 'msg': msg}) + return + elif value == 'review': + if test['status'] != 'private': + msg = 'Not allowed to submit to review' + self.finish_request({'code': 403, 'msg': msg}) + return + + if not test['sut_label']: + msg = 'Please fill out SUT version before submission' + self.finish_request({'code': 403, 'msg': msg}) + return + + query['owner'] = curr_user + db_keys.append('owner') + + test_query = { + 'id': test['id'], + '$or': [ + {'status': 'review'}, + {'status': 'approved'}, + {'status': 'not approved'} + ] + } + record = yield dbapi.db_find_one("tests", test_query) + if record: + msg = ('{} has already submitted one record with the same ' + 'Test ID: {}'.format(record['owner'], test['id'])) + self.finish_request({'code': 403, 'msg': msg}) + return + else: + query['owner'] = curr_user + db_keys.append('owner') + + logging.debug("before _update 2") + self._update(query=query, db_keys=db_keys) + + @gen.coroutine + def check_auth(self, item, value): + logging.debug('check_auth') + user = self.get_secure_cookie(auth_const.OPENID) + query = {} + if item == "status": + if value == "private" or value == "review": + logging.debug('check review') + query['user_id'] = user + data = yield dbapi.db_find_one('applications', query) + if not data: + logging.debug('not found') + raise gen.Return((False, message.no_auth())) + if value == "approve" or value == "not approved": + logging.debug('check approve') + query['role'] = {"$regex": ".*reviewer.*"} + query['openid'] = user + data = yield dbapi.db_find_one('users', query) + if not data: + logging.debug('not found') + raise gen.Return((False, message.no_auth())) + raise gen.Return((True, {})) diff --git a/opnfv_testapi/resources/test_models.py b/opnfv_testapi/resources/test_models.py new file mode 100644 index 0000000..3829cd6 --- /dev/null +++ b/opnfv_testapi/resources/test_models.py @@ -0,0 +1,90 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from opnfv_testapi.resources import models +from opnfv_testapi.tornado_swagger import swagger + +from datetime import datetime + + +@swagger.model() +class TestCreateRequest(models.ModelBase): + """ + @property trust_indicator: + @ptype trust_indicator: L{TI} + """ + def __init__(self, + _id=None, + owner=None, + results=[], + public="false", + review="false", + status="private", + shared=[]): + self._id = _id + self.owner = owner + self.results = results.copy() + self.public = public + self.review = review + self.upload_date = datetime.now() + self.status = status + self.shared = shared + + +class ResultUpdateRequest(models.ModelBase): + """ + @property trust_indicator: + @ptype trust_indicator: L{TI} + """ + def __init__(self, trust_indicator=None): + self.trust_indicator = trust_indicator + + +@swagger.model() +class Test(models.ModelBase): + """ + @property trust_indicator: used for long duration test case + @ptype trust_indicator: L{TI} + """ + def __init__(self, + _id=None, + owner=None, + results=[], + public="false", + review="false", + status="private", + shared=[], + filename="", + label="", + sut_label="", + trust_indicator=None): + self._id = _id + self.owner = owner + self.results = results + self.public = public + self.review = review + self.upload_date = datetime.now() + self.status = status + self.shared = shared + self.filename = filename + self.label = label + self.sut_label = sut_label + + +@swagger.model() +class Tests(models.ModelBase): + """ + @property tests: + @ptype tests: C{list} of L{Test} + """ + def __init__(self): + self.tests = list() + + @staticmethod + def attr_parser(): + return {'tests': Test} diff --git a/opnfv_testapi/resources/testcase_handlers.py b/opnfv_testapi/resources/testcase_handlers.py new file mode 100644 index 0000000..9399326 --- /dev/null +++ b/opnfv_testapi/resources/testcase_handlers.py @@ -0,0 +1,103 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +from opnfv_testapi.resources import handlers +from opnfv_testapi.resources import testcase_models +from opnfv_testapi.tornado_swagger import swagger + + +class GenericTestcaseHandler(handlers.GenericApiHandler): + def __init__(self, application, request, **kwargs): + super(GenericTestcaseHandler, self).__init__(application, + request, + **kwargs) + self.table = self.db_testcases + self.table_cls = testcase_models.Testcase + + +class TestcaseCLHandler(GenericTestcaseHandler): + @swagger.operation(nickname="listAllTestCases") + def get(self, project_name): + """ + @description: list all testcases of a project by project_name + @return 200: return all testcases of this project, + empty list is no testcase exist in this project + @rtype: L{TestCases} + """ + self._list(query={'project_name': project_name}) + + @swagger.operation(nickname="createTestCase") + def post(self, project_name): + """ + @description: create a testcase of a project by project_name + @param body: testcase to be created + @type body: L{TestcaseCreateRequest} + @in body: body + @rtype: L{CreateResponse} + @return 200: testcase is created in this project. + @raise 403: project not exist + or testcase already exists in this project + @raise 400: body or name not provided + """ + def project_query(): + return {'name': project_name} + + def testcase_query(): + return {'project_name': project_name, + 'name': self.json_args.get('name')} + miss_fields = ['name'] + carriers = [(self.db_projects, project_query)] + self._create(miss_fields=miss_fields, + carriers=carriers, + query=testcase_query, + project_name=project_name) + + +class TestcaseGURHandler(GenericTestcaseHandler): + @swagger.operation(nickname='getTestCaseByName') + def get(self, project_name, case_name): + """ + @description: get a single testcase + by case_name and project_name + @rtype: L{Testcase} + @return 200: testcase exist + @raise 404: testcase not exist + """ + query = dict() + query['project_name'] = project_name + query["name"] = case_name + self._get_one(query=query) + + @swagger.operation(nickname="updateTestCaseByName") + def put(self, project_name, case_name): + """ + @description: update a single testcase + by project_name and case_name + @param body: testcase to be updated + @type body: L{TestcaseUpdateRequest} + @in body: body + @rtype: L{Project} + @return 200: update success + @raise 404: testcase or project not exist + @raise 403: new testcase name already exist in project + or nothing to update + """ + query = {'project_name': project_name, 'name': case_name} + db_keys = ['name', 'project_name'] + self._update(query=query, db_keys=db_keys) + + @swagger.operation(nickname='deleteTestCaseByName') + def delete(self, project_name, case_name): + """ + @description: delete a testcase by project_name and case_name + @return 200: delete success + @raise 404: testcase not exist + """ + query = {'project_name': project_name, 'name': case_name} + self._delete(query=query) diff --git a/opnfv_testapi/resources/testcase_models.py b/opnfv_testapi/resources/testcase_models.py new file mode 100644 index 0000000..2379dfc --- /dev/null +++ b/opnfv_testapi/resources/testcase_models.py @@ -0,0 +1,95 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from opnfv_testapi.resources import models +from opnfv_testapi.tornado_swagger import swagger + + +@swagger.model() +class TestcaseCreateRequest(models.ModelBase): + def __init__(self, name, url=None, description=None, + catalog_description=None, tier=None, ci_loop=None, + criteria=None, blocking=None, dependencies=None, run=None, + domains=None, tags=None, version=None): + self.name = name + self.url = url + self.description = description + self.catalog_description = catalog_description + self.tier = tier + self.ci_loop = ci_loop + self.criteria = criteria + self.blocking = blocking + self.dependencies = dependencies + self.run = run + self.domains = domains + self.tags = tags + self.version = version + self.trust = "Silver" + + +@swagger.model() +class TestcaseUpdateRequest(models.ModelBase): + def __init__(self, name=None, description=None, project_name=None, + catalog_description=None, tier=None, ci_loop=None, + criteria=None, blocking=None, dependencies=None, run=None, + domains=None, tags=None, version=None, trust=None): + self.name = name + self.description = description + self.catalog_description = catalog_description + self.project_name = project_name + self.tier = tier + self.ci_loop = ci_loop + self.criteria = criteria + self.blocking = blocking + self.dependencies = dependencies + self.run = run + self.domains = domains + self.tags = tags + self.version = version + self.trust = trust + + +@swagger.model() +class Testcase(models.ModelBase): + def __init__(self, _id=None, name=None, project_name=None, + description=None, url=None, creation_date=None, + catalog_description=None, tier=None, ci_loop=None, + criteria=None, blocking=None, dependencies=None, run=None, + domains=None, tags=None, version=None, + trust=None): + self._id = None + self.name = None + self.project_name = None + self.description = None + self.catalog_description = None + self.url = None + self.creation_date = None + self.tier = None + self.ci_loop = None + self.criteria = None + self.blocking = None + self.dependencies = None + self.run = None + self.domains = None + self.tags = None + self.version = None + self.trust = None + + +@swagger.model() +class Testcases(models.ModelBase): + """ + @property testcases: + @ptype testcases: C{list} of L{Testcase} + """ + def __init__(self): + self.testcases = list() + + @staticmethod + def attr_parser(): + return {'testcases': Testcase} diff --git a/opnfv_testapi/router/__init__.py b/opnfv_testapi/router/__init__.py new file mode 100644 index 0000000..3fc79f1 --- /dev/null +++ b/opnfv_testapi/router/__init__.py @@ -0,0 +1,9 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +__author__ = 'serena' diff --git a/opnfv_testapi/router/url_mappings.py b/opnfv_testapi/router/url_mappings.py new file mode 100644 index 0000000..e1d4c18 --- /dev/null +++ b/opnfv_testapi/router/url_mappings.py @@ -0,0 +1,45 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +from opnfv_testapi.resources import handlers +from opnfv_testapi.resources import result_handlers +from opnfv_testapi.resources import test_handlers +from opnfv_testapi.resources import application_handlers +from opnfv_testapi.resources import sut_handlers +from opnfv_testapi.ui.auth import sign +from opnfv_testapi.ui.auth import user + +mappings = [ + (r"/versions", handlers.VersionHandler), + + (r"/api/v1/results/upload", result_handlers.ResultsUploadHandler), + (r"/api/v1/results/([^/]+)", result_handlers.ResultsGURHandler), + + (r"/api/v1/tests", test_handlers.TestsCLHandler), + (r"/api/v1/tests/([^/]+)", test_handlers.TestsGURHandler), + + (r"/api/v1/cvp/applications/getlogo/([^/]+)", + application_handlers.ApplicationsGetLogoHandler), + (r"/api/v1/cvp/applications/uploadlogo", + application_handlers.ApplicationsLogoHandler), + (r"/api/v1/cvp/applications", application_handlers.ApplicationsCLHandler), + (r"/api/v1/cvp/applications/([^/]+)", + application_handlers.ApplicationsGURHandler), + + (r"/api/v1/suts/hardware/([^/]+)", sut_handlers.HardwareHandler), + + + (r'/api/v1/auth/signin', sign.SigninHandler), + (r'/api/v1/auth/signin_return', sign.SigninReturnHandler), + (r'/api/v1/auth/signin_return_jira', sign.SigninReturnJiraHandler), + (r'/api/v1/auth/signin_return_cas', sign.SigninReturnCasHandler), + (r'/api/v1/auth/signout', sign.SignoutHandler), + (r'/api/v1/profile', user.ProfileHandler), + +] diff --git a/opnfv_testapi/tests/__init__.py b/opnfv_testapi/tests/__init__.py new file mode 100644 index 0000000..9f28b0b --- /dev/null +++ b/opnfv_testapi/tests/__init__.py @@ -0,0 +1 @@ +__author__ = 'serena' diff --git a/opnfv_testapi/tests/unit/__init__.py b/opnfv_testapi/tests/unit/__init__.py new file mode 100644 index 0000000..3fc79f1 --- /dev/null +++ b/opnfv_testapi/tests/unit/__init__.py @@ -0,0 +1,9 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +__author__ = 'serena' diff --git a/opnfv_testapi/tests/unit/common/__init__.py b/opnfv_testapi/tests/unit/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opnfv_testapi/tests/unit/common/noparam.ini b/opnfv_testapi/tests/unit/common/noparam.ini new file mode 100644 index 0000000..fda2a09 --- /dev/null +++ b/opnfv_testapi/tests/unit/common/noparam.ini @@ -0,0 +1,16 @@ +# to add a new parameter in the config file, +# the CONF object in config.ini must be updated +[mongo] +# URL of the mongo DB +# Mongo auth url => mongodb://user1:pwd1@host1/?authSource=db1 +url = mongodb://127.0.0.1:27017/ + +[api] +# Listening port +port = 8000 +# With debug_on set to true, error traces will be shown in HTTP responses +debug = True +authenticate = False + +[swagger] +base_url = http://localhost:8000 diff --git a/opnfv_testapi/tests/unit/common/normal.ini b/opnfv_testapi/tests/unit/common/normal.ini new file mode 100644 index 0000000..77cc6c6 --- /dev/null +++ b/opnfv_testapi/tests/unit/common/normal.ini @@ -0,0 +1,17 @@ +# to add a new parameter in the config file, +# the CONF object in config.ini must be updated +[mongo] +# URL of the mongo DB +# Mongo auth url => mongodb://user1:pwd1@host1/?authSource=db1 +url = mongodb://127.0.0.1:27017/ +dbname = test_results_collection + +[api] +# Listening port +port = 8000 +# With debug_on set to true, error traces will be shown in HTTP responses +debug = True +authenticate = False + +[swagger] +base_url = http://localhost:8000 diff --git a/opnfv_testapi/tests/unit/common/nosection.ini b/opnfv_testapi/tests/unit/common/nosection.ini new file mode 100644 index 0000000..9988fc0 --- /dev/null +++ b/opnfv_testapi/tests/unit/common/nosection.ini @@ -0,0 +1,11 @@ +# to add a new parameter in the config file, +# the CONF object in config.ini must be updated +[api] +# Listening port +port = 8000 +# With debug_on set to true, error traces will be shown in HTTP responses +debug = True +authenticate = False + +[swagger] +base_url = http://localhost:8000 diff --git a/opnfv_testapi/tests/unit/common/notboolean.ini b/opnfv_testapi/tests/unit/common/notboolean.ini new file mode 100644 index 0000000..b3f3276 --- /dev/null +++ b/opnfv_testapi/tests/unit/common/notboolean.ini @@ -0,0 +1,17 @@ +# to add a new parameter in the config file, +# the CONF object in config.ini must be updated +[mongo] +# URL of the mongo DB +# Mongo auth url => mongodb://user1:pwd1@host1/?authSource=db1 +url = mongodb://127.0.0.1:27017/ +dbname = test_results_collection + +[api] +# Listening port +port = 8000 +# With debug_on set to true, error traces will be shown in HTTP responses +debug = True +authenticate = notboolean + +[swagger] +base_url = http://localhost:8000 diff --git a/opnfv_testapi/tests/unit/common/notint.ini b/opnfv_testapi/tests/unit/common/notint.ini new file mode 100644 index 0000000..d1b752a --- /dev/null +++ b/opnfv_testapi/tests/unit/common/notint.ini @@ -0,0 +1,17 @@ +# to add a new parameter in the config file, +# the CONF object in config.ini must be updated +[mongo] +# URL of the mongo DB +# Mongo auth url => mongodb://user1:pwd1@host1/?authSource=db1 +url = mongodb://127.0.0.1:27017/ +dbname = test_results_collection + +[api] +# Listening port +port = notint +# With debug_on set to true, error traces will be shown in HTTP responses +debug = True +authenticate = False + +[swagger] +base_url = http://localhost:8000 diff --git a/opnfv_testapi/tests/unit/common/test_config.py b/opnfv_testapi/tests/unit/common/test_config.py new file mode 100644 index 0000000..cc8743c --- /dev/null +++ b/opnfv_testapi/tests/unit/common/test_config.py @@ -0,0 +1,15 @@ +import argparse + + +def test_config_normal(mocker, config_normal): + mocker.patch( + 'argparse.ArgumentParser.parse_known_args', + return_value=(argparse.Namespace(config_file=config_normal), None)) + from opnfv_testapi.common import config + CONF = config.Config() + assert CONF.mongo_url == 'mongodb://127.0.0.1:27017/' + assert CONF.mongo_dbname == 'test_results_collection' + assert CONF.api_port == 8000 + assert CONF.api_debug is True + assert CONF.api_authenticate is False + assert CONF.swagger_base_url == 'http://localhost:8000' diff --git a/opnfv_testapi/tests/unit/conftest.py b/opnfv_testapi/tests/unit/conftest.py new file mode 100644 index 0000000..feff1da --- /dev/null +++ b/opnfv_testapi/tests/unit/conftest.py @@ -0,0 +1,8 @@ +from os import path + +import pytest + + +@pytest.fixture +def config_normal(): + return path.join(path.dirname(__file__), 'common/normal.ini') diff --git a/opnfv_testapi/tests/unit/executor.py b/opnfv_testapi/tests/unit/executor.py new file mode 100644 index 0000000..b8f696c --- /dev/null +++ b/opnfv_testapi/tests/unit/executor.py @@ -0,0 +1,97 @@ +############################################################################## +# Copyright (c) 2017 ZTE Corp +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import functools +import httplib + + +def upload(excepted_status, excepted_response): + def _upload(create_request): + @functools.wraps(create_request) + def wrap(self): + request = create_request(self) + status, body = self.upload(request) + if excepted_status == httplib.OK: + getattr(self, excepted_response)(body) + else: + self.assertIn(excepted_response, body) + return wrap + return _upload + + +def create(excepted_status, excepted_response): + def _create(create_request): + @functools.wraps(create_request) + def wrap(self): + request = create_request(self) + status, body = self.create(request) + if excepted_status == httplib.OK: + getattr(self, excepted_response)(body) + else: + self.assertIn(excepted_response, body) + return wrap + return _create + + +def get(excepted_status, excepted_response): + def _get(get_request): + @functools.wraps(get_request) + def wrap(self): + request = get_request(self) + status, body = self.get(request) + if excepted_status == httplib.OK: + getattr(self, excepted_response)(body) + else: + self.assertIn(excepted_response, body) + return wrap + return _get + + +def update(excepted_status, excepted_response): + def _update(update_request): + @functools.wraps(update_request) + def wrap(self): + request, resource = update_request(self) + status, body = self.update(request, resource) + if excepted_status == httplib.OK: + getattr(self, excepted_response)(request, body) + else: + self.assertIn(excepted_response, body) + return wrap + return _update + + +def delete(excepted_status, excepted_response): + def _delete(delete_request): + @functools.wraps(delete_request) + def wrap(self): + request = delete_request(self) + if isinstance(request, tuple): + status, body = self.delete(request[0], *(request[1])) + else: + status, body = self.delete(request) + if excepted_status == httplib.OK: + getattr(self, excepted_response)(body) + else: + self.assertIn(excepted_response, body) + return wrap + return _delete + + +def query(excepted_status, excepted_response, number=0): + def _query(get_request): + @functools.wraps(get_request) + def wrap(self): + request = get_request(self) + status, body = self.query(request) + if excepted_status == httplib.OK: + getattr(self, excepted_response)(body, number) + else: + self.assertIn(excepted_response, body) + return wrap + return _query diff --git a/opnfv_testapi/tests/unit/fake_pymongo.py b/opnfv_testapi/tests/unit/fake_pymongo.py new file mode 100644 index 0000000..0ca83df --- /dev/null +++ b/opnfv_testapi/tests/unit/fake_pymongo.py @@ -0,0 +1,284 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from operator import itemgetter + +from bson.objectid import ObjectId +from concurrent.futures import ThreadPoolExecutor + + +def thread_execute(method, *args, **kwargs): + with ThreadPoolExecutor(max_workers=2) as executor: + result = executor.submit(method, *args, **kwargs) + return result + + +class MemCursor(object): + def __init__(self, collection): + self.collection = collection + self.length = len(self.collection) + self.sorted = [] + + def _is_next_exist(self): + return self.length != 0 + + @property + def fetch_next(self): + return thread_execute(self._is_next_exist) + + def next_object(self): + self.length -= 1 + return self.collection.pop() + + def sort(self, key_or_list): + for k, v in key_or_list.iteritems(): + if v == -1: + reverse = True + else: + reverse = False + + self.collection = sorted(self.collection, + key=itemgetter(k), reverse=reverse) + return self + + def limit(self, limit): + if limit != 0 and limit < len(self.collection): + self.collection = self.collection[0: limit] + self.length = limit + return self + + def skip(self, skip): + if skip < self.length and (skip > 0): + self.collection = self.collection[self.length - skip: -1] + self.length -= skip + elif skip >= self.length: + self.collection = [] + self.length = 0 + return self + + def _count(self): + return self.length + + def count(self): + return thread_execute(self._count) + + +class MemDb(object): + + def __init__(self, name): + self.name = name + self.contents = [] + pass + + def _find_one(self, spec_or_id=None, *args): + if spec_or_id is not None and not isinstance(spec_or_id, dict): + spec_or_id = {"_id": spec_or_id} + if '_id' in spec_or_id: + spec_or_id['_id'] = str(spec_or_id['_id']) + cursor = self._find(spec_or_id, *args) + for result in cursor: + return result + return None + + def find_one(self, spec_or_id=None, *args): + return thread_execute(self._find_one, spec_or_id, *args) + + def _insert(self, doc_or_docs, check_keys=True): + + docs = doc_or_docs + return_one = False + if isinstance(docs, dict): + return_one = True + docs = [docs] + + if check_keys: + for doc in docs: + self._check_keys(doc) + + ids = [] + for doc in docs: + if '_id' not in doc: + doc['_id'] = str(ObjectId()) + if not self._find_one(doc['_id']): + ids.append(doc['_id']) + self.contents.append(doc_or_docs) + + if len(ids) == 0: + return None + if return_one: + return ids[0] + else: + return ids + + def insert(self, doc_or_docs, check_keys=True): + return thread_execute(self._insert, doc_or_docs, check_keys) + + @staticmethod + def _compare_date(spec, value): + gte = True + lt = False + for k, v in spec.iteritems(): + if k == '$gte' and value < v: + gte = False + elif k == '$lt' and value < v: + lt = True + return gte and lt + + def _in(self, content, *args): + if self.name == 'scenarios': + return self._in_scenarios(content, *args) + else: + return self._in_others(content, *args) + + def _in_scenarios_installer(self, installer, content): + hit = False + for s_installer in content['installers']: + if installer == s_installer['installer']: + hit = True + + return hit + + def _in_scenarios_version(self, version, content): + hit = False + for s_installer in content['installers']: + for s_version in s_installer['versions']: + if version == s_version['version']: + hit = True + return hit + + def _in_scenarios_project(self, project, content): + hit = False + for s_installer in content['installers']: + for s_version in s_installer['versions']: + for s_project in s_version['projects']: + if project == s_project['project']: + hit = True + + return hit + + def _in_scenarios(self, content, *args): + for arg in args: + for k, v in arg.iteritems(): + if k == 'installers': + for inner in v.values(): + for i_k, i_v in inner.iteritems(): + if i_k == 'installer': + return self._in_scenarios_installer(i_v, + content) + elif i_k == 'versions.version': + return self._in_scenarios_version(i_v, + content) + elif i_k == 'versions.projects.project': + return self._in_scenarios_project(i_v, + content) + elif content.get(k, None) != v: + return False + + return True + + def _in_others(self, content, *args): + for arg in args: + for k, v in arg.iteritems(): + if k == 'start_date': + if not MemDb._compare_date(v, content.get(k)): + return False + elif k == 'trust_indicator.current': + if content.get('trust_indicator').get('current') != v: + return False + elif not isinstance(v, dict) and content.get(k, None) != v: + return False + return True + + def _find(self, *args): + res = [] + for content in self.contents: + if self._in(content, *args): + res.append(content) + + return res + + def find(self, *args): + return MemCursor(self._find(*args)) + + def _aggregate(self, *args, **kwargs): + res = self.contents + print args + for arg in args[0]: + for k, v in arg.iteritems(): + if k == '$match': + res = self._find(v) + cursor = MemCursor(res) + for arg in args[0]: + for k, v in arg.iteritems(): + if k == '$sort': + cursor = cursor.sort(v) + elif k == '$skip': + cursor = cursor.skip(v) + elif k == '$limit': + cursor = cursor.limit(v) + return cursor + + def aggregate(self, *args, **kwargs): + return self._aggregate(*args, **kwargs) + + def _update(self, spec, document, check_keys=True): + updated = False + + if check_keys: + self._check_keys(document) + + for index in range(len(self.contents)): + content = self.contents[index] + if self._in(content, spec): + for k, v in document.iteritems(): + updated = True + content[k] = v + self.contents[index] = content + return updated + + def update(self, spec, document, check_keys=True): + return thread_execute(self._update, spec, document, check_keys) + + def _remove(self, spec_or_id=None): + if spec_or_id is None: + self.contents = [] + if not isinstance(spec_or_id, dict): + spec_or_id = {'_id': spec_or_id} + for index in range(len(self.contents)): + content = self.contents[index] + if self._in(content, spec_or_id): + del self.contents[index] + return True + return False + + def remove(self, spec_or_id=None): + return thread_execute(self._remove, spec_or_id) + + def clear(self): + self._remove() + + def _check_keys(self, doc): + for key in doc.keys(): + if '.' in key: + raise NameError('key {} must not contain .'.format(key)) + if key.startswith('$'): + raise NameError('key {} must not start with $'.format(key)) + if isinstance(doc.get(key), dict): + self._check_keys(doc.get(key)) + + +def __getattr__(name): + return globals()[name] + + +pods = MemDb('pods') +projects = MemDb('projects') +testcases = MemDb('testcases') +results = MemDb('results') +scenarios = MemDb('scenarios') +tokens = MemDb('tokens') diff --git a/opnfv_testapi/tests/unit/resources/__init__.py b/opnfv_testapi/tests/unit/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opnfv_testapi/tests/unit/resources/scenario-c1.json b/opnfv_testapi/tests/unit/resources/scenario-c1.json new file mode 100644 index 0000000..1878022 --- /dev/null +++ b/opnfv_testapi/tests/unit/resources/scenario-c1.json @@ -0,0 +1,38 @@ +{ + "name": "nosdn-nofeature-ha", + "installers": + [ + { + "installer": "apex", + "versions": + [ + { + "owner": "Luke", + "version": "master", + "projects": + [ + { + "project": "functest", + "customs": [ "healthcheck", "vping_ssh"], + "scores": + [ + { + "date": "2017-01-08 22:46:44", + "score": "12/14" + } + + ], + "trust_indicators": [] + }, + { + "project": "yardstick", + "customs": [], + "scores": [], + "trust_indicators": [] + } + ] + } + ] + } + ] +} diff --git a/opnfv_testapi/tests/unit/resources/scenario-c2.json b/opnfv_testapi/tests/unit/resources/scenario-c2.json new file mode 100644 index 0000000..b6a3b83 --- /dev/null +++ b/opnfv_testapi/tests/unit/resources/scenario-c2.json @@ -0,0 +1,73 @@ +{ + "name": "odl_2-nofeature-ha", + "installers": + [ + { + "installer": "fuel", + "versions": + [ + { + "owner": "Lucky", + "version": "colorado", + "projects": + [ + { + "project": "functest", + "customs": [ "healthcheck", "vping_ssh"], + "scores": [], + "trust_indicators": [ + { + "date": "2017-01-18 22:46:44", + "status": "silver" + } + + ] + }, + { + "project": "yardstick", + "customs": ["suite-a"], + "scores": [ + { + "date": "2017-01-08 22:46:44", + "score": "0" + } + ], + "trust_indicators": [ + { + "date": "2017-01-18 22:46:44", + "status": "gold" + } + ] + } + ] + }, + { + "owner": "Luke", + "version": "colorado", + "projects": + [ + { + "project": "functest", + "customs": [ "healthcheck", "vping_ssh"], + "scores": + [ + { + "date": "2017-01-09 22:46:44", + "score": "11/14" + } + + ], + "trust_indicators": [] + }, + { + "project": "yardstick", + "customs": [], + "scores": [], + "trust_indicators": [] + } + ] + } + ] + } + ] +} diff --git a/opnfv_testapi/tests/unit/resources/test_base.py b/opnfv_testapi/tests/unit/resources/test_base.py new file mode 100644 index 0000000..dcec4e9 --- /dev/null +++ b/opnfv_testapi/tests/unit/resources/test_base.py @@ -0,0 +1,161 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import json +from os import path + +import mock +from tornado import testing + +from opnfv_testapi.resources import models +from opnfv_testapi.tests.unit import fake_pymongo + + +class TestBase(testing.AsyncHTTPTestCase): + headers = {'Content-Type': 'application/json; charset=UTF-8'} + + def setUp(self): + self._patch_server() + self.basePath = '' + self.create_res = models.CreateResponse + self.get_res = None + self.list_res = None + self.update_res = None + self.req_d = None + self.req_e = None + self.addCleanup(self._clear) + super(TestBase, self).setUp() + + def tearDown(self): + self.db_patcher.stop() + self.config_patcher.stop() + + def _patch_server(self): + import argparse + config = path.join(path.dirname(__file__), '../common/normal.ini') + self.config_patcher = mock.patch( + 'argparse.ArgumentParser.parse_known_args', + return_value=(argparse.Namespace(config_file=config), None)) + self.db_patcher = mock.patch('opnfv_testapi.db.api.DB', + fake_pymongo) + self.config_patcher.start() + self.db_patcher.start() + + def set_config_file(self): + self.config_file = 'normal.ini' + + def get_app(self): + from opnfv_testapi.cmd import server + return server.make_app() + + def create_d(self, *args): + return self.create(self.req_d, *args) + + def create_e(self, *args): + return self.create(self.req_e, *args) + + def create(self, req=None, *args): + return self.create_help(self.basePath, req, *args) + + def create_help(self, uri, req, *args): + if req and not isinstance(req, str) and hasattr(req, 'format'): + req = req.format() + res = self.fetch(self._update_uri(uri, *args), + method='POST', + body=json.dumps(req), + headers=self.headers) + + return self._get_return(res, self.create_res) + + def get(self, *args): + res = self.fetch(self._get_uri(*args), + method='GET', + headers=self.headers) + + def inner(): + new_args, num = self._get_valid_args(*args) + return self.get_res \ + if num != self._need_arg_num(self.basePath) else self.list_res + return self._get_return(res, inner()) + + def query(self, query): + res = self.fetch(self._get_query_uri(query), + method='GET', + headers=self.headers) + return self._get_return(res, self.list_res) + + def update(self, new=None, *args): + if new: + new = new.format() + res = self.fetch(self._get_uri(*args), + method='PUT', + body=json.dumps(new), + headers=self.headers) + return self._get_return(res, self.update_res) + + def delete(self, *args): + res = self.fetch(self._get_uri(*args), + method='DELETE', + headers=self.headers) + return res.code, res.body + + @staticmethod + def _get_valid_args(*args): + new_args = tuple(['%s' % arg for arg in args if arg is not None]) + return new_args, len(new_args) + + def _need_arg_num(self, uri): + return uri.count('%s') + + def _get_query_uri(self, query): + return self.basePath + '?' + query if query else self.basePath + + def _get_uri(self, *args): + return self._update_uri(self.basePath, *args) + + def _update_uri(self, uri, *args): + r_uri = uri + new_args, num = self._get_valid_args(*args) + if num != self._need_arg_num(uri): + r_uri += '/%s' + + return r_uri % tuple(['%s' % arg for arg in new_args]) + + def _get_return(self, res, cls): + code = res.code + body = res.body + return code, self._get_return_body(code, body, cls) + + @staticmethod + def _get_return_body(code, body, cls): + return cls.from_dict(json.loads(body)) if code < 300 and cls else body + + def assert_href(self, body): + self.assertIn(self.basePath, body.href) + + def assert_create_body(self, body, req=None, *args): + import inspect + if not req: + req = self.req_d + resource_name = '' + if inspect.isclass(req): + resource_name = req.name + elif isinstance(req, dict): + resource_name = req['name'] + elif isinstance(req, str): + resource_name = json.loads(req)['name'] + new_args = args + tuple([resource_name]) + self.assertIn(self._get_uri(*new_args), body.href) + + @staticmethod + def _clear(): + fake_pymongo.pods.clear() + fake_pymongo.projects.clear() + fake_pymongo.testcases.clear() + fake_pymongo.results.clear() + fake_pymongo.scenarios.clear() diff --git a/opnfv_testapi/tests/unit/resources/test_fake_pymongo.py b/opnfv_testapi/tests/unit/resources/test_fake_pymongo.py new file mode 100644 index 0000000..1ebc96f --- /dev/null +++ b/opnfv_testapi/tests/unit/resources/test_fake_pymongo.py @@ -0,0 +1,123 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import unittest + +from tornado import gen +from tornado import testing +from tornado import web + +from opnfv_testapi.tests.unit import fake_pymongo + + +class MyTest(testing.AsyncHTTPTestCase): + def setUp(self): + super(MyTest, self).setUp() + self.db = fake_pymongo + self.addCleanup(self._clear) + self.io_loop.run_sync(self.fixture_setup) + + def get_app(self): + return web.Application() + + @gen.coroutine + def fixture_setup(self): + self.test1 = {'_id': '1', 'name': 'test1'} + self.test2 = {'name': 'test2'} + yield self.db.pods.insert({'_id': '1', 'name': 'test1'}) + yield self.db.pods.insert({'name': 'test2'}) + + @testing.gen_test + def test_find_one(self): + user = yield self.db.pods.find_one({'name': 'test1'}) + self.assertEqual(user, self.test1) + self.db.pods.remove() + + @testing.gen_test + def test_find(self): + cursor = self.db.pods.find() + names = [] + while (yield cursor.fetch_next): + ob = cursor.next_object() + names.append(ob.get('name')) + self.assertItemsEqual(names, ['test1', 'test2']) + + @testing.gen_test + def test_update(self): + yield self.db.pods.update({'_id': '1'}, {'name': 'new_test1'}) + user = yield self.db.pods.find_one({'_id': '1'}) + self.assertEqual(user.get('name', None), 'new_test1') + + def test_update_dot_error(self): + self._update_assert({'_id': '1', 'name': {'1. name': 'test1'}}, + 'key 1. name must not contain .') + + def test_update_dot_no_error(self): + self._update_assert({'_id': '1', 'name': {'1. name': 'test1'}}, + None, + check_keys=False) + + def test_update_dollar_error(self): + self._update_assert({'_id': '1', 'name': {'$name': 'test1'}}, + 'key $name must not start with $') + + def test_update_dollar_no_error(self): + self._update_assert({'_id': '1', 'name': {'$name': 'test1'}}, + None, + check_keys=False) + + @testing.gen_test + def test_remove(self): + yield self.db.pods.remove({'_id': '1'}) + user = yield self.db.pods.find_one({'_id': '1'}) + self.assertIsNone(user) + + def test_insert_dot_error(self): + self._insert_assert({'_id': '1', '2. name': 'test1'}, + 'key 2. name must not contain .') + + def test_insert_dot_no_error(self): + self._insert_assert({'_id': '1', '2. name': 'test1'}, + None, + check_keys=False) + + def test_insert_dollar_error(self): + self._insert_assert({'_id': '1', '$name': 'test1'}, + 'key $name must not start with $') + + def test_insert_dollar_no_error(self): + self._insert_assert({'_id': '1', '$name': 'test1'}, + None, + check_keys=False) + + def _clear(self): + self.db.pods.clear() + + def _update_assert(self, docs, error=None, **kwargs): + self._db_assert('update', error, {'_id': '1'}, docs, **kwargs) + + def _insert_assert(self, docs, error=None, **kwargs): + self._db_assert('insert', error, docs, **kwargs) + + @testing.gen_test + def _db_assert(self, method, error, *args, **kwargs): + name_error = None + try: + yield self._eval_pods_db(method, *args, **kwargs) + except NameError as err: + name_error = err.args[0] + finally: + self.assertEqual(name_error, error) + + def _eval_pods_db(self, method, *args, **kwargs): + table_obj = vars(self.db)['pods'] + return table_obj.__getattribute__(method)(*args, **kwargs) + + +if __name__ == '__main__': + unittest.main() diff --git a/opnfv_testapi/tests/unit/resources/test_pod.py b/opnfv_testapi/tests/unit/resources/test_pod.py new file mode 100644 index 0000000..cb4f1d9 --- /dev/null +++ b/opnfv_testapi/tests/unit/resources/test_pod.py @@ -0,0 +1,90 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import httplib +import unittest + +from opnfv_testapi.common import message +from opnfv_testapi.resources import pod_models +from opnfv_testapi.tests.unit import executor +from opnfv_testapi.tests.unit.resources import test_base as base + + +class TestPodBase(base.TestBase): + def setUp(self): + super(TestPodBase, self).setUp() + self.req_d = pod_models.PodCreateRequest('zte-1', 'virtual', + 'zte pod 1', 'ci-pod') + self.req_e = pod_models.PodCreateRequest('zte-2', 'metal', 'zte pod 2') + self.get_res = pod_models.Pod + self.list_res = pod_models.Pods + self.basePath = '/api/v1/pods' + + def assert_get_body(self, pod, req=None): + if not req: + req = self.req_d + self.assertEqual(pod.name, req.name) + self.assertEqual(pod.mode, req.mode) + self.assertEqual(pod.details, req.details) + self.assertEqual(pod.role, req.role) + self.assertIsNotNone(pod.creation_date) + self.assertIsNotNone(pod._id) + + +class TestPodCreate(TestPodBase): + @executor.create(httplib.BAD_REQUEST, message.no_body()) + def test_withoutBody(self): + return None + + @executor.create(httplib.BAD_REQUEST, message.missing('name')) + def test_emptyName(self): + return pod_models.PodCreateRequest('') + + @executor.create(httplib.BAD_REQUEST, message.missing('name')) + def test_noneName(self): + return pod_models.PodCreateRequest(None) + + @executor.create(httplib.OK, 'assert_create_body') + def test_success(self): + return self.req_d + + @executor.create(httplib.FORBIDDEN, message.exist_base) + def test_alreadyExist(self): + self.create_d() + return self.req_d + + +class TestPodGet(TestPodBase): + def setUp(self): + super(TestPodGet, self).setUp() + self.create_d() + self.create_e() + + @executor.get(httplib.NOT_FOUND, message.not_found_base) + def test_notExist(self): + return 'notExist' + + @executor.get(httplib.OK, 'assert_get_body') + def test_getOne(self): + return self.req_d.name + + @executor.get(httplib.OK, '_assert_list') + def test_list(self): + return None + + def _assert_list(self, body): + self.assertEqual(len(body.pods), 2) + for pod in body.pods: + if self.req_d.name == pod.name: + self.assert_get_body(pod) + else: + self.assert_get_body(pod, self.req_e) + + +if __name__ == '__main__': + unittest.main() diff --git a/opnfv_testapi/tests/unit/resources/test_project.py b/opnfv_testapi/tests/unit/resources/test_project.py new file mode 100644 index 0000000..0622ba8 --- /dev/null +++ b/opnfv_testapi/tests/unit/resources/test_project.py @@ -0,0 +1,137 @@ +import httplib +import unittest + +from opnfv_testapi.common import message +from opnfv_testapi.resources import project_models +from opnfv_testapi.tests.unit import executor +from opnfv_testapi.tests.unit.resources import test_base as base + + +class TestProjectBase(base.TestBase): + def setUp(self): + super(TestProjectBase, self).setUp() + self.req_d = project_models.ProjectCreateRequest('vping', + 'vping-ssh test') + self.req_e = project_models.ProjectCreateRequest('doctor', + 'doctor test') + self.get_res = project_models.Project + self.list_res = project_models.Projects + self.update_res = project_models.Project + self.basePath = '/api/v1/projects' + + def assert_body(self, project, req=None): + if not req: + req = self.req_d + self.assertEqual(project.name, req.name) + self.assertEqual(project.description, req.description) + self.assertIsNotNone(project._id) + self.assertIsNotNone(project.creation_date) + + +class TestProjectCreate(TestProjectBase): + @executor.create(httplib.BAD_REQUEST, message.no_body()) + def test_withoutBody(self): + return None + + @executor.create(httplib.BAD_REQUEST, message.missing('name')) + def test_emptyName(self): + return project_models.ProjectCreateRequest('') + + @executor.create(httplib.BAD_REQUEST, message.missing('name')) + def test_noneName(self): + return project_models.ProjectCreateRequest(None) + + @executor.create(httplib.OK, 'assert_create_body') + def test_success(self): + return self.req_d + + @executor.create(httplib.FORBIDDEN, message.exist_base) + def test_alreadyExist(self): + self.create_d() + return self.req_d + + +class TestProjectGet(TestProjectBase): + def setUp(self): + super(TestProjectGet, self).setUp() + self.create_d() + self.create_e() + + @executor.get(httplib.NOT_FOUND, message.not_found_base) + def test_notExist(self): + return 'notExist' + + @executor.get(httplib.OK, 'assert_body') + def test_getOne(self): + return self.req_d.name + + @executor.get(httplib.OK, '_assert_list') + def test_list(self): + return None + + def _assert_list(self, body): + for project in body.projects: + if self.req_d.name == project.name: + self.assert_body(project) + else: + self.assert_body(project, self.req_e) + + +class TestProjectUpdate(TestProjectBase): + def setUp(self): + super(TestProjectUpdate, self).setUp() + _, d_body = self.create_d() + _, get_res = self.get(self.req_d.name) + self.index_d = get_res._id + self.create_e() + + @executor.update(httplib.BAD_REQUEST, message.no_body()) + def test_withoutBody(self): + return None, 'noBody' + + @executor.update(httplib.NOT_FOUND, message.not_found_base) + def test_notFound(self): + return self.req_e, 'notFound' + + @executor.update(httplib.FORBIDDEN, message.exist_base) + def test_newNameExist(self): + return self.req_e, self.req_d.name + + @executor.update(httplib.FORBIDDEN, message.no_update()) + def test_noUpdate(self): + return self.req_d, self.req_d.name + + @executor.update(httplib.OK, '_assert_update') + def test_success(self): + req = project_models.ProjectUpdateRequest('newName', 'new description') + return req, self.req_d.name + + def _assert_update(self, req, body): + self.assertEqual(self.index_d, body._id) + self.assert_body(body, req) + _, new_body = self.get(req.name) + self.assertEqual(self.index_d, new_body._id) + self.assert_body(new_body, req) + + +class TestProjectDelete(TestProjectBase): + def setUp(self): + super(TestProjectDelete, self).setUp() + self.create_d() + + @executor.delete(httplib.NOT_FOUND, message.not_found_base) + def test_notFound(self): + return 'notFound' + + @executor.delete(httplib.OK, '_assert_delete') + def test_success(self): + return self.req_d.name + + def _assert_delete(self, body): + self.assertEqual(body, '') + code, body = self.get(self.req_d.name) + self.assertEqual(code, httplib.NOT_FOUND) + + +if __name__ == '__main__': + unittest.main() diff --git a/opnfv_testapi/tests/unit/resources/test_result.py b/opnfv_testapi/tests/unit/resources/test_result.py new file mode 100644 index 0000000..1e83ed3 --- /dev/null +++ b/opnfv_testapi/tests/unit/resources/test_result.py @@ -0,0 +1,410 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import copy +import httplib +import unittest +from datetime import datetime, timedelta +import json + +from opnfv_testapi.common import message +from opnfv_testapi.resources import pod_models +from opnfv_testapi.resources import project_models +from opnfv_testapi.resources import result_models +from opnfv_testapi.resources import testcase_models +from opnfv_testapi.tests.unit import executor +from opnfv_testapi.tests.unit.resources import test_base as base + + +class Details(object): + def __init__(self, timestart=None, duration=None, status=None): + self.timestart = timestart + self.duration = duration + self.status = status + self.items = [{'item1': 1}, {'item2': 2}] + + def format(self): + return { + "timestart": self.timestart, + "duration": self.duration, + "status": self.status, + 'items': [{'item1': 1}, {'item2': 2}] + } + + @staticmethod + def from_dict(a_dict): + + if a_dict is None: + return None + + t = Details() + t.timestart = a_dict.get('timestart') + t.duration = a_dict.get('duration') + t.status = a_dict.get('status') + t.items = a_dict.get('items') + return t + + +class TestResultBase(base.TestBase): + def setUp(self): + self.pod = 'zte-pod1' + self.project = 'functest' + self.case = 'vPing' + self.installer = 'fuel' + self.version = 'C' + self.build_tag = 'v3.0' + self.scenario = 'odl-l2' + self.criteria = 'passed' + self.trust_indicator = result_models.TI(0.7) + self.start_date = str(datetime.now()) + self.stop_date = str(datetime.now() + timedelta(minutes=1)) + self.update_date = str(datetime.now() + timedelta(days=1)) + self.update_step = -0.05 + super(TestResultBase, self).setUp() + self.details = Details(timestart='0', duration='9s', status='OK') + self.req_d = result_models.ResultCreateRequest( + pod_name=self.pod, + project_name=self.project, + case_name=self.case, + installer=self.installer, + version=self.version, + start_date=self.start_date, + stop_date=self.stop_date, + details=self.details.format(), + build_tag=self.build_tag, + scenario=self.scenario, + criteria=self.criteria, + trust_indicator=self.trust_indicator) + self.get_res = result_models.TestResult + self.list_res = result_models.TestResults + self.update_res = result_models.TestResult + self.basePath = '/api/v1/results' + self.req_pod = pod_models.PodCreateRequest( + self.pod, + 'metal', + 'zte pod 1') + self.req_project = project_models.ProjectCreateRequest( + self.project, + 'vping test') + self.req_testcase = testcase_models.TestcaseCreateRequest( + self.case, + '/cases/vping', + 'vping-ssh test') + self.create_help('/api/v1/pods', self.req_pod) + self.create_help('/api/v1/projects', self.req_project) + self.create_help('/api/v1/projects/%s/cases', + self.req_testcase, + self.project) + + def assert_res(self, result, req=None): + if req is None: + req = self.req_d + self.assertEqual(result.pod_name, req.pod_name) + self.assertEqual(result.project_name, req.project_name) + self.assertEqual(result.case_name, req.case_name) + self.assertEqual(result.installer, req.installer) + self.assertEqual(result.version, req.version) + details_req = Details.from_dict(req.details) + details_res = Details.from_dict(result.details) + self.assertEqual(details_res.duration, details_req.duration) + self.assertEqual(details_res.timestart, details_req.timestart) + self.assertEqual(details_res.status, details_req.status) + self.assertEqual(details_res.items, details_req.items) + self.assertEqual(result.build_tag, req.build_tag) + self.assertEqual(result.scenario, req.scenario) + self.assertEqual(result.criteria, req.criteria) + self.assertEqual(result.start_date, req.start_date) + self.assertEqual(result.stop_date, req.stop_date) + self.assertIsNotNone(result._id) + ti = result.trust_indicator + self.assertEqual(ti.current, req.trust_indicator.current) + if ti.histories: + history = ti.histories[0] + self.assertEqual(history.date, self.update_date) + self.assertEqual(history.step, self.update_step) + + def _create_d(self): + _, res = self.create_d() + return res.href.split('/')[-1] + + def upload(self, req): + if req and not isinstance(req, str) and hasattr(req, 'format'): + req = req.format() + res = self.fetch(self.basePath + '/upload', + method='POST', + body=json.dumps(req), + headers=self.headers) + + return self._get_return(res, self.create_res) + + +class TestResultUpload(TestResultBase): + @executor.upload(httplib.BAD_REQUEST, message.key_error('file')) + def test_filenotfind(self): + return None + + +class TestResultCreate(TestResultBase): + @executor.create(httplib.BAD_REQUEST, message.no_body()) + def test_nobody(self): + return None + + @executor.create(httplib.BAD_REQUEST, message.missing('pod_name')) + def test_podNotProvided(self): + req = self.req_d + req.pod_name = None + return req + + @executor.create(httplib.BAD_REQUEST, message.missing('project_name')) + def test_projectNotProvided(self): + req = self.req_d + req.project_name = None + return req + + @executor.create(httplib.BAD_REQUEST, message.missing('case_name')) + def test_testcaseNotProvided(self): + req = self.req_d + req.case_name = None + return req + + @executor.create(httplib.FORBIDDEN, message.not_found_base) + def test_noPod(self): + req = self.req_d + req.pod_name = 'notExistPod' + return req + + @executor.create(httplib.FORBIDDEN, message.not_found_base) + def test_noProject(self): + req = self.req_d + req.project_name = 'notExistProject' + return req + + @executor.create(httplib.FORBIDDEN, message.not_found_base) + def test_noTestcase(self): + req = self.req_d + req.case_name = 'notExistTestcase' + return req + + @executor.create(httplib.OK, 'assert_href') + def test_success(self): + return self.req_d + + @executor.create(httplib.OK, 'assert_href') + def test_key_with_doc(self): + req = copy.deepcopy(self.req_d) + req.details = {'1.name': 'dot_name'} + return req + + @executor.create(httplib.OK, '_assert_no_ti') + def test_no_ti(self): + req = result_models.ResultCreateRequest(pod_name=self.pod, + project_name=self.project, + case_name=self.case, + installer=self.installer, + version=self.version, + start_date=self.start_date, + stop_date=self.stop_date, + details=self.details.format(), + build_tag=self.build_tag, + scenario=self.scenario, + criteria=self.criteria) + self.actual_req = req + return req + + def _assert_no_ti(self, body): + _id = body.href.split('/')[-1] + code, body = self.get(_id) + self.assert_res(body, self.actual_req) + + +class TestResultGet(TestResultBase): + def setUp(self): + super(TestResultGet, self).setUp() + self.req_10d_before = self._create_changed_date(days=-10) + self.req_d_id = self._create_d() + self.req_10d_later = self._create_changed_date(days=10) + + @executor.get(httplib.OK, 'assert_res') + def test_getOne(self): + return self.req_d_id + + @executor.query(httplib.OK, '_query_success', 3) + def test_queryPod(self): + return self._set_query('pod') + + @executor.query(httplib.OK, '_query_success', 3) + def test_queryProject(self): + return self._set_query('project') + + @executor.query(httplib.OK, '_query_success', 3) + def test_queryTestcase(self): + return self._set_query('case') + + @executor.query(httplib.OK, '_query_success', 3) + def test_queryVersion(self): + return self._set_query('version') + + @executor.query(httplib.OK, '_query_success', 3) + def test_queryInstaller(self): + return self._set_query('installer') + + @executor.query(httplib.OK, '_query_success', 3) + def test_queryBuildTag(self): + return self._set_query('build_tag') + + @executor.query(httplib.OK, '_query_success', 3) + def test_queryScenario(self): + return self._set_query('scenario') + + @executor.query(httplib.OK, '_query_success', 3) + def test_queryTrustIndicator(self): + return self._set_query('trust_indicator') + + @executor.query(httplib.OK, '_query_success', 3) + def test_queryCriteria(self): + return self._set_query('criteria') + + @executor.query(httplib.BAD_REQUEST, message.must_int('period')) + def test_queryPeriodNotInt(self): + return self._set_query('period=a') + + @executor.query(httplib.OK, '_query_period_one', 1) + def test_queryPeriodSuccess(self): + return self._set_query('period=5') + + @executor.query(httplib.BAD_REQUEST, message.must_int('last')) + def test_queryLastNotInt(self): + return self._set_query('last=a') + + @executor.query(httplib.OK, '_query_last_one', 1) + def test_queryLast(self): + return self._set_query('last=1') + + @executor.query(httplib.OK, '_query_success', 4) + def test_queryPublic(self): + self._create_public_data() + return self._set_query('') + + @executor.query(httplib.OK, '_query_success', 1) + def test_queryPrivate(self): + self._create_private_data() + return self._set_query('public=false') + + @executor.query(httplib.OK, '_query_period_one', 1) + def test_combination(self): + return self._set_query('pod', + 'project', + 'case', + 'version', + 'installer', + 'build_tag', + 'scenario', + 'trust_indicator', + 'criteria', + 'period=5') + + @executor.query(httplib.OK, '_query_success', 0) + def test_notFound(self): + return self._set_query('pod=notExistPod', + 'project', + 'case', + 'version', + 'installer', + 'build_tag', + 'scenario', + 'trust_indicator', + 'criteria', + 'period=1') + + @executor.query(httplib.OK, '_query_success', 1) + def test_filterErrorStartdate(self): + self._create_error_start_date(None) + self._create_error_start_date('None') + self._create_error_start_date('null') + self._create_error_start_date('') + return self._set_query('period=5') + + def _query_success(self, body, number): + self.assertEqual(number, len(body.results)) + + def _query_last_one(self, body, number): + self.assertEqual(number, len(body.results)) + self.assert_res(body.results[0], self.req_10d_later) + + def _query_period_one(self, body, number): + self.assertEqual(number, len(body.results)) + self.assert_res(body.results[0], self.req_d) + + def _create_error_start_date(self, start_date): + req = copy.deepcopy(self.req_d) + req.start_date = start_date + self.create(req) + return req + + def _create_changed_date(self, **kwargs): + req = copy.deepcopy(self.req_d) + req.start_date = datetime.now() + timedelta(**kwargs) + req.stop_date = str(req.start_date + timedelta(minutes=10)) + req.start_date = str(req.start_date) + self.create(req) + return req + + def _create_public_data(self, **kwargs): + req = copy.deepcopy(self.req_d) + req.public = 'true' + self.create(req) + return req + + def _create_private_data(self, **kwargs): + req = copy.deepcopy(self.req_d) + req.public = 'false' + self.create(req) + return req + + def _set_query(self, *args): + def get_value(arg): + return self.__getattribute__(arg) \ + if arg != 'trust_indicator' else self.trust_indicator.current + uri = '' + for arg in args: + if arg: + if '=' in arg: + uri += arg + '&' + else: + uri += '{}={}&'.format(arg, get_value(arg)) + return uri[0: -1] + + +class TestResultUpdate(TestResultBase): + def setUp(self): + super(TestResultUpdate, self).setUp() + self.req_d_id = self._create_d() + + @executor.update(httplib.OK, '_assert_update_ti') + def test_success(self): + new_ti = copy.deepcopy(self.trust_indicator) + new_ti.current += self.update_step + new_ti.histories.append( + result_models.TIHistory(self.update_date, self.update_step)) + new_data = copy.deepcopy(self.req_d) + new_data.trust_indicator = new_ti + update = result_models.ResultUpdateRequest(trust_indicator=new_ti) + self.update_req = new_data + return update, self.req_d_id + + def _assert_update_ti(self, request, body): + ti = body.trust_indicator + self.assertEqual(ti.current, request.trust_indicator.current) + if ti.histories: + history = ti.histories[0] + self.assertEqual(history.date, self.update_date) + self.assertEqual(history.step, self.update_step) + + +if __name__ == '__main__': + unittest.main() diff --git a/opnfv_testapi/tests/unit/resources/test_scenario.py b/opnfv_testapi/tests/unit/resources/test_scenario.py new file mode 100644 index 0000000..bd72067 --- /dev/null +++ b/opnfv_testapi/tests/unit/resources/test_scenario.py @@ -0,0 +1,360 @@ +import functools +import httplib +import json +import os +from copy import deepcopy +from datetime import datetime + +import opnfv_testapi.resources.scenario_models as models +from opnfv_testapi.common import message +from opnfv_testapi.tests.unit.resources import test_base as base + + +class TestScenarioBase(base.TestBase): + def setUp(self): + super(TestScenarioBase, self).setUp() + self.get_res = models.Scenario + self.list_res = models.Scenarios + self.basePath = '/api/v1/scenarios' + self.req_d = self._load_request('scenario-c1.json') + self.req_2 = self._load_request('scenario-c2.json') + + def tearDown(self): + pass + + def assert_body(self, project, req=None): + pass + + @staticmethod + def _load_request(f_req): + abs_file = os.path.join(os.path.dirname(__file__), f_req) + with open(abs_file, 'r') as f: + loader = json.load(f) + f.close() + return loader + + def create_return_name(self, req): + _, res = self.create(req) + return res.href.split('/')[-1] + + def assert_res(self, code, scenario, req=None): + self.assertEqual(code, httplib.OK) + if req is None: + req = self.req_d + self.assertIsNotNone(scenario._id) + self.assertIsNotNone(scenario.creation_date) + + scenario == models.Scenario.from_dict(req) + + @staticmethod + def _set_query(*args): + uri = '' + for arg in args: + uri += arg + '&' + return uri[0: -1] + + def _get_and_assert(self, name, req=None): + code, body = self.get(name) + self.assert_res(code, body, req) + + +class TestScenarioCreate(TestScenarioBase): + def test_withoutBody(self): + (code, body) = self.create() + self.assertEqual(code, httplib.BAD_REQUEST) + + def test_emptyName(self): + req_empty = models.ScenarioCreateRequest('') + (code, body) = self.create(req_empty) + self.assertEqual(code, httplib.BAD_REQUEST) + self.assertIn(message.missing('name'), body) + + def test_noneName(self): + req_none = models.ScenarioCreateRequest(None) + (code, body) = self.create(req_none) + self.assertEqual(code, httplib.BAD_REQUEST) + self.assertIn(message.missing('name'), body) + + def test_success(self): + (code, body) = self.create_d() + self.assertEqual(code, httplib.OK) + self.assert_create_body(body) + + def test_alreadyExist(self): + self.create_d() + (code, body) = self.create_d() + self.assertEqual(code, httplib.FORBIDDEN) + self.assertIn(message.exist_base, body) + + +class TestScenarioGet(TestScenarioBase): + def setUp(self): + super(TestScenarioGet, self).setUp() + self.scenario_1 = self.create_return_name(self.req_d) + self.scenario_2 = self.create_return_name(self.req_2) + + def test_getByName(self): + self._get_and_assert(self.scenario_1, self.req_d) + + def test_getAll(self): + self._query_and_assert(query=None, reqs=[self.req_d, self.req_2]) + + def test_queryName(self): + query = self._set_query('name=nosdn-nofeature-ha') + self._query_and_assert(query, reqs=[self.req_d]) + + def test_queryInstaller(self): + query = self._set_query('installer=apex') + self._query_and_assert(query, reqs=[self.req_d]) + + def test_queryVersion(self): + query = self._set_query('version=master') + self._query_and_assert(query, reqs=[self.req_d]) + + def test_queryProject(self): + query = self._set_query('project=functest') + self._query_and_assert(query, reqs=[self.req_d, self.req_2]) + + def test_queryCombination(self): + query = self._set_query('name=nosdn-nofeature-ha', + 'installer=apex', + 'version=master', + 'project=functest') + + self._query_and_assert(query, reqs=[self.req_d]) + + def _query_and_assert(self, query, found=True, reqs=None): + code, body = self.query(query) + if not found: + self.assertEqual(code, httplib.OK) + self.assertEqual(0, len(body.scenarios)) + else: + self.assertEqual(len(reqs), len(body.scenarios)) + for req in reqs: + for scenario in body.scenarios: + if req['name'] == scenario.name: + self.assert_res(code, scenario, req) + + +class TestScenarioUpdate(TestScenarioBase): + def setUp(self): + super(TestScenarioUpdate, self).setUp() + self.scenario = self.create_return_name(self.req_d) + self.scenario_2 = self.create_return_name(self.req_2) + + def _execute(set_update): + @functools.wraps(set_update) + def magic(self): + update, scenario = set_update(self, deepcopy(self.req_d)) + self._update_and_assert(update, scenario) + return magic + + def _update(expected): + def _update(set_update): + @functools.wraps(set_update) + def wrap(self): + update, scenario = set_update(self, deepcopy(self.req_d)) + code, body = self.update(update, self.scenario) + getattr(self, expected)(code, scenario) + return wrap + return _update + + @_update('_success') + def test_renameScenario(self, scenario): + new_name = 'nosdn-nofeature-noha' + scenario['name'] = new_name + update_req = models.ScenarioUpdateRequest(field='name', + op='update', + locate={}, + term={'name': new_name}) + return update_req, scenario + + @_update('_forbidden') + def test_renameScenario_exist(self, scenario): + new_name = self.scenario_2 + scenario['name'] = new_name + update_req = models.ScenarioUpdateRequest(field='name', + op='update', + locate={}, + term={'name': new_name}) + return update_req, scenario + + @_update('_bad_request') + def test_renameScenario_noName(self, scenario): + new_name = self.scenario_2 + scenario['name'] = new_name + update_req = models.ScenarioUpdateRequest(field='name', + op='update', + locate={}, + term={}) + return update_req, scenario + + @_execute + def test_addInstaller(self, scenario): + add = models.ScenarioInstaller(installer='daisy', versions=list()) + scenario['installers'].append(add.format()) + update = models.ScenarioUpdateRequest(field='installer', + op='add', + locate={}, + term=add.format()) + return update, scenario + + @_execute + def test_deleteInstaller(self, scenario): + scenario['installers'] = filter(lambda f: f['installer'] != 'apex', + scenario['installers']) + + update = models.ScenarioUpdateRequest(field='installer', + op='delete', + locate={'installer': 'apex'}) + return update, scenario + + @_execute + def test_addVersion(self, scenario): + add = models.ScenarioVersion(version='danube', projects=list()) + scenario['installers'][0]['versions'].append(add.format()) + update = models.ScenarioUpdateRequest(field='version', + op='add', + locate={'installer': 'apex'}, + term=add.format()) + return update, scenario + + @_execute + def test_deleteVersion(self, scenario): + scenario['installers'][0]['versions'] = filter( + lambda f: f['version'] != 'master', + scenario['installers'][0]['versions']) + + update = models.ScenarioUpdateRequest(field='version', + op='delete', + locate={'installer': 'apex', + 'version': 'master'}) + return update, scenario + + @_execute + def test_changeOwner(self, scenario): + scenario['installers'][0]['versions'][0]['owner'] = 'lucy' + + update = models.ScenarioUpdateRequest(field='owner', + op='update', + locate={'installer': 'apex', + 'version': 'master'}, + term={'owner': 'lucy'}) + return update, scenario + + @_execute + def test_addProject(self, scenario): + add = models.ScenarioProject(project='qtip').format() + scenario['installers'][0]['versions'][0]['projects'].append(add) + update = models.ScenarioUpdateRequest(field='project', + op='add', + locate={'installer': 'apex', + 'version': 'master'}, + term=add) + return update, scenario + + @_execute + def test_deleteProject(self, scenario): + scenario['installers'][0]['versions'][0]['projects'] = filter( + lambda f: f['project'] != 'functest', + scenario['installers'][0]['versions'][0]['projects']) + + update = models.ScenarioUpdateRequest(field='project', + op='delete', + locate={ + 'installer': 'apex', + 'version': 'master', + 'project': 'functest'}) + return update, scenario + + @_execute + def test_addCustoms(self, scenario): + add = ['odl', 'parser', 'vping_ssh'] + projects = scenario['installers'][0]['versions'][0]['projects'] + functest = filter(lambda f: f['project'] == 'functest', projects)[0] + functest['customs'] = ['healthcheck', 'odl', 'parser', 'vping_ssh'] + update = models.ScenarioUpdateRequest(field='customs', + op='add', + locate={ + 'installer': 'apex', + 'version': 'master', + 'project': 'functest'}, + term=add) + return update, scenario + + @_execute + def test_deleteCustoms(self, scenario): + projects = scenario['installers'][0]['versions'][0]['projects'] + functest = filter(lambda f: f['project'] == 'functest', projects)[0] + functest['customs'] = ['healthcheck'] + update = models.ScenarioUpdateRequest(field='customs', + op='delete', + locate={ + 'installer': 'apex', + 'version': 'master', + 'project': 'functest'}, + term=['vping_ssh']) + return update, scenario + + @_execute + def test_addScore(self, scenario): + add = models.ScenarioScore(date=str(datetime.now()), score='11/12') + projects = scenario['installers'][0]['versions'][0]['projects'] + functest = filter(lambda f: f['project'] == 'functest', projects)[0] + functest['scores'].append(add.format()) + update = models.ScenarioUpdateRequest(field='score', + op='add', + locate={ + 'installer': 'apex', + 'version': 'master', + 'project': 'functest'}, + term=add.format()) + return update, scenario + + @_execute + def test_addTi(self, scenario): + add = models.ScenarioTI(date=str(datetime.now()), status='gold') + projects = scenario['installers'][0]['versions'][0]['projects'] + functest = filter(lambda f: f['project'] == 'functest', projects)[0] + functest['trust_indicators'].append(add.format()) + update = models.ScenarioUpdateRequest(field='trust_indicator', + op='add', + locate={ + 'installer': 'apex', + 'version': 'master', + 'project': 'functest'}, + term=add.format()) + return update, scenario + + def _update_and_assert(self, update_req, new_scenario, name=None): + code, _ = self.update(update_req, self.scenario) + self.assertEqual(code, httplib.OK) + self._get_and_assert(_none_default(name, self.scenario), + new_scenario) + + def _success(self, status, new_scenario): + self.assertEqual(status, httplib.OK) + self._get_and_assert(new_scenario.get('name'), new_scenario) + + def _forbidden(self, status, new_scenario): + self.assertEqual(status, httplib.FORBIDDEN) + + def _bad_request(self, status, new_scenario): + self.assertEqual(status, httplib.BAD_REQUEST) + + +class TestScenarioDelete(TestScenarioBase): + def test_notFound(self): + code, body = self.delete('notFound') + self.assertEqual(code, httplib.NOT_FOUND) + + def test_success(self): + scenario = self.create_return_name(self.req_d) + code, _ = self.delete(scenario) + self.assertEqual(code, httplib.OK) + code, _ = self.get(scenario) + self.assertEqual(code, httplib.NOT_FOUND) + + +def _none_default(check, default): + return check if check else default diff --git a/opnfv_testapi/tests/unit/resources/test_testcase.py b/opnfv_testapi/tests/unit/resources/test_testcase.py new file mode 100644 index 0000000..4f2bc2a --- /dev/null +++ b/opnfv_testapi/tests/unit/resources/test_testcase.py @@ -0,0 +1,201 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import copy +import httplib +import unittest + +from opnfv_testapi.common import message +from opnfv_testapi.resources import project_models +from opnfv_testapi.resources import testcase_models +from opnfv_testapi.tests.unit import executor +from opnfv_testapi.tests.unit.resources import test_base as base + + +class TestCaseBase(base.TestBase): + def setUp(self): + super(TestCaseBase, self).setUp() + self.req_d = testcase_models.TestcaseCreateRequest('vping_1', + '/cases/vping_1', + 'vping-ssh test') + self.req_e = testcase_models.TestcaseCreateRequest('doctor_1', + '/cases/doctor_1', + 'create doctor') + self.update_d = testcase_models.TestcaseUpdateRequest('vping_1', + 'vping-ssh test', + 'functest') + self.update_e = testcase_models.TestcaseUpdateRequest('doctor_1', + 'create doctor', + 'functest') + self.get_res = testcase_models.Testcase + self.list_res = testcase_models.Testcases + self.update_res = testcase_models.Testcase + self.basePath = '/api/v1/projects/%s/cases' + self.create_project() + + def assert_body(self, case, req=None): + if not req: + req = self.req_d + self.assertEqual(case.name, req.name) + self.assertEqual(case.description, req.description) + self.assertEqual(case.url, req.url) + self.assertIsNotNone(case._id) + self.assertIsNotNone(case.creation_date) + + def assert_update_body(self, old, new, req=None): + if not req: + req = self.req_d + self.assertEqual(new.name, req.name) + self.assertEqual(new.description, req.description) + self.assertEqual(new.url, old.url) + self.assertIsNotNone(new._id) + self.assertIsNotNone(new.creation_date) + + def create_project(self): + req_p = project_models.ProjectCreateRequest('functest', + 'vping-ssh test') + self.create_help('/api/v1/projects', req_p) + self.project = req_p.name + + def create_d(self): + return super(TestCaseBase, self).create_d(self.project) + + def create_e(self): + return super(TestCaseBase, self).create_e(self.project) + + def get(self, case=None): + return super(TestCaseBase, self).get(self.project, case) + + def create(self, req=None, *args): + return super(TestCaseBase, self).create(req, self.project) + + def update(self, new=None, case=None): + return super(TestCaseBase, self).update(new, self.project, case) + + def delete(self, case): + return super(TestCaseBase, self).delete(self.project, case) + + +class TestCaseCreate(TestCaseBase): + @executor.create(httplib.BAD_REQUEST, message.no_body()) + def test_noBody(self): + return None + + @executor.create(httplib.FORBIDDEN, message.not_found_base) + def test_noProject(self): + self.project = 'noProject' + return self.req_d + + @executor.create(httplib.BAD_REQUEST, message.missing('name')) + def test_emptyName(self): + req_empty = testcase_models.TestcaseCreateRequest('') + return req_empty + + @executor.create(httplib.BAD_REQUEST, message.missing('name')) + def test_noneName(self): + req_none = testcase_models.TestcaseCreateRequest(None) + return req_none + + @executor.create(httplib.OK, '_assert_success') + def test_success(self): + return self.req_d + + def _assert_success(self, body): + self.assert_create_body(body, self.req_d, self.project) + + @executor.create(httplib.FORBIDDEN, message.exist_base) + def test_alreadyExist(self): + self.create_d() + return self.req_d + + +class TestCaseGet(TestCaseBase): + def setUp(self): + super(TestCaseGet, self).setUp() + self.create_d() + self.create_e() + + @executor.get(httplib.NOT_FOUND, message.not_found_base) + def test_notExist(self): + return 'notExist' + + @executor.get(httplib.OK, 'assert_body') + def test_getOne(self): + return self.req_d.name + + @executor.get(httplib.OK, '_list') + def test_list(self): + return None + + def _list(self, body): + for case in body.testcases: + if self.req_d.name == case.name: + self.assert_body(case) + else: + self.assert_body(case, self.req_e) + + +class TestCaseUpdate(TestCaseBase): + def setUp(self): + super(TestCaseUpdate, self).setUp() + self.create_d() + + @executor.update(httplib.BAD_REQUEST, message.no_body()) + def test_noBody(self): + return None, 'noBody' + + @executor.update(httplib.NOT_FOUND, message.not_found_base) + def test_notFound(self): + return self.update_e, 'notFound' + + @executor.update(httplib.FORBIDDEN, message.exist_base) + def test_newNameExist(self): + self.create_e() + return self.update_e, self.req_d.name + + @executor.update(httplib.FORBIDDEN, message.no_update()) + def test_noUpdate(self): + return self.update_d, self.req_d.name + + @executor.update(httplib.OK, '_update_success') + def test_success(self): + return self.update_e, self.req_d.name + + @executor.update(httplib.OK, '_update_success') + def test_with_dollar(self): + update = copy.deepcopy(self.update_d) + update.description = {'2. change': 'dollar change'} + return update, self.req_d.name + + def _update_success(self, request, body): + self.assert_update_body(self.req_d, body, request) + _, new_body = self.get(request.name) + self.assert_update_body(self.req_d, new_body, request) + + +class TestCaseDelete(TestCaseBase): + def setUp(self): + super(TestCaseDelete, self).setUp() + self.create_d() + + @executor.delete(httplib.NOT_FOUND, message.not_found_base) + def test_notFound(self): + return 'notFound' + + @executor.delete(httplib.OK, '_delete_success') + def test_success(self): + return self.req_d.name + + def _delete_success(self, body): + self.assertEqual(body, '') + code, body = self.get(self.req_d.name) + self.assertEqual(code, httplib.NOT_FOUND) + + +if __name__ == '__main__': + unittest.main() diff --git a/opnfv_testapi/tests/unit/resources/test_token.py b/opnfv_testapi/tests/unit/resources/test_token.py new file mode 100644 index 0000000..940e256 --- /dev/null +++ b/opnfv_testapi/tests/unit/resources/test_token.py @@ -0,0 +1,114 @@ +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 + +import httplib +import unittest + +from tornado import web + +from opnfv_testapi.common import message +from opnfv_testapi.resources import project_models +from opnfv_testapi.tests.unit import executor +from opnfv_testapi.tests.unit import fake_pymongo +from opnfv_testapi.tests.unit.resources import test_base as base + + +class TestToken(base.TestBase): + def get_app(self): + from opnfv_testapi.router import url_mappings + return web.Application( + url_mappings.mappings, + db=fake_pymongo, + debug=True, + auth=True + ) + + +class TestTokenCreateProject(TestToken): + def setUp(self): + super(TestTokenCreateProject, self).setUp() + self.req_d = project_models.ProjectCreateRequest('vping') + fake_pymongo.tokens.insert({"access_token": "12345"}) + self.basePath = '/api/v1/projects' + + @executor.create(httplib.FORBIDDEN, message.invalid_token()) + def test_projectCreateTokenInvalid(self): + self.headers['X-Auth-Token'] = '1234' + return self.req_d + + @executor.create(httplib.UNAUTHORIZED, message.unauthorized()) + def test_projectCreateTokenUnauthorized(self): + if 'X-Auth-Token' in self.headers: + self.headers.pop('X-Auth-Token') + return self.req_d + + @executor.create(httplib.OK, '_create_success') + def test_projectCreateTokenSuccess(self): + self.headers['X-Auth-Token'] = '12345' + return self.req_d + + def _create_success(self, body): + self.assertIn('CreateResponse', str(type(body))) + + +class TestTokenDeleteProject(TestToken): + def setUp(self): + super(TestTokenDeleteProject, self).setUp() + self.req_d = project_models.ProjectCreateRequest('vping') + fake_pymongo.tokens.insert({"access_token": "12345"}) + self.basePath = '/api/v1/projects' + self.headers['X-Auth-Token'] = '12345' + self.create_d() + + @executor.delete(httplib.FORBIDDEN, message.invalid_token()) + def test_projectDeleteTokenIvalid(self): + self.headers['X-Auth-Token'] = '1234' + return self.req_d.name + + @executor.delete(httplib.UNAUTHORIZED, message.unauthorized()) + def test_projectDeleteTokenUnauthorized(self): + self.headers.pop('X-Auth-Token') + return self.req_d.name + + @executor.delete(httplib.OK, '_delete_success') + def test_projectDeleteTokenSuccess(self): + return self.req_d.name + + def _delete_success(self, body): + self.assertEqual('', body) + + +class TestTokenUpdateProject(TestToken): + def setUp(self): + super(TestTokenUpdateProject, self).setUp() + self.req_d = project_models.ProjectCreateRequest('vping') + fake_pymongo.tokens.insert({"access_token": "12345"}) + self.basePath = '/api/v1/projects' + self.headers['X-Auth-Token'] = '12345' + self.create_d() + + @executor.update(httplib.FORBIDDEN, message.invalid_token()) + def test_projectUpdateTokenIvalid(self): + self.headers['X-Auth-Token'] = '1234' + req = project_models.ProjectUpdateRequest('newName', 'new description') + return req, self.req_d.name + + @executor.update(httplib.UNAUTHORIZED, message.unauthorized()) + def test_projectUpdateTokenUnauthorized(self): + self.headers.pop('X-Auth-Token') + req = project_models.ProjectUpdateRequest('newName', 'new description') + return req, self.req_d.name + + @executor.update(httplib.OK, '_update_success') + def test_projectUpdateTokenSuccess(self): + req = project_models.ProjectUpdateRequest('newName', 'new description') + return req, self.req_d.name + + def _update_success(self, request, body): + self.assertIn(request.name, body) + + +if __name__ == '__main__': + unittest.main() diff --git a/opnfv_testapi/tests/unit/resources/test_version.py b/opnfv_testapi/tests/unit/resources/test_version.py new file mode 100644 index 0000000..51fed11 --- /dev/null +++ b/opnfv_testapi/tests/unit/resources/test_version.py @@ -0,0 +1,36 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import httplib +import unittest + +from opnfv_testapi.resources import models +from opnfv_testapi.tests.unit import executor +from opnfv_testapi.tests.unit.resources import test_base as base + + +class TestVersionBase(base.TestBase): + def setUp(self): + super(TestVersionBase, self).setUp() + self.list_res = models.Versions + self.basePath = '/versions' + + +class TestVersion(TestVersionBase): + @executor.get(httplib.OK, '_get_success') + def test_success(self): + return None + + def _get_success(self, body): + self.assertEqual(len(body.versions), 1) + self.assertEqual(body.versions[0].version, 'v1.0') + self.assertEqual(body.versions[0].description, 'basics') + + +if __name__ == '__main__': + unittest.main() diff --git a/opnfv_testapi/tornado_swagger/README.md b/opnfv_testapi/tornado_swagger/README.md new file mode 100644 index 0000000..d815f21 --- /dev/null +++ b/opnfv_testapi/tornado_swagger/README.md @@ -0,0 +1,273 @@ +# tornado-swagger + +## What is tornado-swagger? +tornado is a wrapper for tornado which enables swagger-ui support. + +In essense, you just need to wrap the Api instance and add a few python decorators to +get full swagger support.http://swagger.io/ + + +## How to use: + + +```python +from tornado.web import RequestHandler, HTTPError +from tornado_swagger import swagger + +swagger.docs() + +# You may decorate your operation with @swagger.operation and use docs to inform information +class ItemNoParamHandler(GenericApiHandler): + @swagger.operation(nickname='create') + def post(self): + """ + @param body: create test results for a item. + @type body: L{Item} + @return 200: item is created. + @raise 400: invalid input + """ + +# Operations not decorated with @swagger.operation do not get added to the swagger docs + +class ItemNoParamHandler(GenericApiHandler): + def options(self): + """ + I'm not visible in the swagger docs + """ + pass + + +# Then you use swagger.Application instead of tornado.web.Application +# and do other operations as usual + +def make_app(): + return swagger.Application([ + (r"/items", ItemNoParamHandler), + (r"/items/([^/]+)", ItemHandler), + (r"/items/([^/]+)/cases/([^/]+)", ItemOptionParamHandler), + ]) + +# You define models like this: +@swagger.model +class Item: + """ + @descriptin: + This is an example of a model class that has parameters in its constructor + and the fields in the swagger spec are derived from the parameters to __init__. + @notes: + In this case we would have property1, property2 as required parameters + and property3 as optional parameter. + @property property3: Item decription + @ptype property3: L{PropertySubclass} + """ + def __init__(self, property1, property2=None): + self.property1 = property1 + self.property2 = property2 + +# Swagger json: + "models": { + "Item": { + "description": "A description...", + "id": "Item", + "required": [ + "property1", + ], + "properties": [ + "property1": { + "type": "string" + }, + "property2": { + "type": "string" + "default": null + } + ] + } + } + +# If you declare an __init__ method with meaningful arguments +# then those args could be used to deduce the swagger model fields. +# just as shown above + +# if you declare an @property in docs, this property property2 will also be used +# to deduce the swagger model fields +class Item: + """ + @property property3: Item description + """ + def __init__(self, property1, property2): + self.property1 = property1 + self.property2 = property2 + +# Swagger json: + "models": { + "Item": { + "description": "A description...", + "id": "Item", + "required": [ + "property1", + ], + "properties": [ + "property1": { + "type": "string" + }, + "property2": { + "type": "string" + } + "property3": { + "type": "string" + } + ] + } + } + +# if you declare an argument with @ptype, the type of this argument will be specified +# rather than the default 'string' +class Item: + """ + @ptype property3: L{PropertySubclass} + """ + def __init__(self, property1, property2, property3=None): + self.property1 = property1 + self.property2 = property2 + self.property3 = property3 + +# Swagger json: + "models": { + "Item": { + "description": "A description...", + "id": "Item", + "required": [ + "property1", + ], + "properties": [ + "property1": { + "type": "string" + }, + "property2": { + "type": "string" + }, + "property3": { + "type": "PropertySubclass" + "default": null + } + ] + } + } + +# if you want to declare an list property, you can do it like this: +class Item: + """ + @ptype property3: L{PropertySubclass} + @ptype property4: C{list} of L{PropertySubclass} + """ + def __init__(self, property1, property2, property3, property4=None): + self.property1 = property1 + self.property2 = property2 + self.property3 = property3 + self.property4 = property4 + +# Swagger json: + "models": { + "Item": { + "description": "A description...", + "id": "Item", + "required": [ + "property1", + ], + "properties": [ + "property1": { + "type": "string" + }, + "property2": { + "type": "string" + }, + "property3": { + "type": "PropertySubclass" + "default": null + }, + "property4": { + "default": null, + "items": { + "type": "PropertySubclass"}, + "type": "array" + } + } + ] + } + } + +# if it is a query: +class ItemQueryHandler(GenericApiHandler): + @swagger.operation(nickname='query') + def get(self): + """ + @param property1: + @type property1: L{string} + @in property1: query + @required property1: False + + @param property2: + @type property2: L{string} + @in property2: query + @required property2: True + @rtype: L{Item} + + @notes: GET /item?property1=1&property2=1 + """ + +# Swagger json: + "apis": [ + { + "operations": [ + { + "parameters": [ + { + "name": "property1", + "dataType": "string", + "paramType": "query", + "description": "" + }, + { + "name": "property2", + "dataType": "string", + "paramType": "query", + "required": true, + "description": "" + } + ], + "responseClass": "Item", + "notes": null, + "responseMessages": [], + "summary": null, + "httpMethod": "GET", + "nickname": "query" + } + ], + "path": "/item", + "description": null + }, + .... + ] +``` + +# Running and testing + +Now run your tornado app + +``` +python main.py +``` + +And visit: + +``` +curl http://ip:port/swagger/spec +``` + +access to web +``` +http://ip:port/swagger/spec.html +``` + +# Passing more metadata to swagger +customized arguments used in creating the 'swagger.docs' object will be supported later diff --git a/opnfv_testapi/tornado_swagger/__init__.py b/opnfv_testapi/tornado_swagger/__init__.py new file mode 100644 index 0000000..363bc38 --- /dev/null +++ b/opnfv_testapi/tornado_swagger/__init__.py @@ -0,0 +1,8 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## diff --git a/opnfv_testapi/tornado_swagger/handlers.py b/opnfv_testapi/tornado_swagger/handlers.py new file mode 100644 index 0000000..e39a9f6 --- /dev/null +++ b/opnfv_testapi/tornado_swagger/handlers.py @@ -0,0 +1,38 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import tornado.web + +from opnfv_testapi.tornado_swagger import settings +from opnfv_testapi.tornado_swagger import views + + +def swagger_handlers(): + prefix = settings.docs_settings.get('swagger_prefix', '/swagger') + if prefix[-1] != '/': + prefix += '/' + + def _path(suffix): + return prefix + suffix + return [ + tornado.web.URLSpec( + _path(r'spec.html$'), + views.SwaggerUIHandler, + settings.docs_settings, + name=settings.API_DOCS_NAME), + tornado.web.URLSpec( + _path(r'resources.json$'), + views.SwaggerResourcesHandler, + settings.docs_settings, + name=settings.RESOURCE_LISTING_NAME), + tornado.web.URLSpec( + _path(r'APIs$'), + views.SwaggerApiHandler, + settings.docs_settings, + name=settings.API_DECLARATION_NAME), + ] diff --git a/opnfv_testapi/tornado_swagger/settings.py b/opnfv_testapi/tornado_swagger/settings.py new file mode 100644 index 0000000..2842261 --- /dev/null +++ b/opnfv_testapi/tornado_swagger/settings.py @@ -0,0 +1,25 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +API_DOCS_NAME = 'swagger-api-docs' +RESOURCE_LISTING_NAME = 'swagger-resource-listing' +API_DECLARATION_NAME = 'swagger-api-declaration' + +docs_settings = { + 'base_url': '', + 'static_path': '', + 'swagger_prefix': '/swagger', + 'api_version': 'v1.0', + 'swagger_version': '1.2', + 'api_key': '', + 'enabled_methods': ['get', 'post', 'put', 'patch', 'delete'], + 'exclude_namespaces': [], +} + +models = [] diff --git a/opnfv_testapi/tornado_swagger/swagger.py b/opnfv_testapi/tornado_swagger/swagger.py new file mode 100644 index 0000000..83f389a --- /dev/null +++ b/opnfv_testapi/tornado_swagger/swagger.py @@ -0,0 +1,291 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from HTMLParser import HTMLParser +from functools import wraps +import inspect + +import epydoc.markup +import tornado.web + +from opnfv_testapi.tornado_swagger import handlers +from opnfv_testapi.tornado_swagger import settings + + +class EpytextParser(HTMLParser): + a_text = False + + def __init__(self, tag): + HTMLParser.__init__(self) + self.tag = tag + self.data = None + + def handle_starttag(self, tag, attr): + if tag == self.tag: + self.a_text = True + + def handle_endtag(self, tag): + if tag == self.tag: + self.a_text = False + + def handle_data(self, data): + if self.a_text: + self.data = data + + def get_data(self): + return self.data + + +class DocParser(object): + def __init__(self): + self.notes = None + self.summary = None + self.responseClass = None + self.responseMessages = [] + self.params = {} + self.properties = {} + + def parse_docstring(self, text): + if text is None: + return + + errors = [] + doc = epydoc.markup.parse(text, markup='epytext', errors=errors) + _, fields = doc.split_fields(errors) + + for field in fields: + tag = field.tag() + arg = field.arg() + body = field.body() + self._get_parser(tag)(arg=arg, body=body) + return doc + + def _get_parser(self, tag): + parser = { + 'param': self._parse_param, + 'type': self._parse_type, + 'in': self._parse_in, + 'required': self._parse_required, + 'rtype': self._parse_rtype, + 'property': self._parse_property, + 'ptype': self._parse_ptype, + 'return': self._parse_return, + 'raise': self._parse_return, + 'notes': self._parse_notes, + 'description': self._parse_description, + } + return parser.get(tag, self._not_supported) + + def _parse_param(self, **kwargs): + arg = kwargs.get('arg', None) + body = self._get_body(**kwargs) + self.params.setdefault(arg, {}).update({ + 'name': arg, + 'description': body, + }) + + if 'paramType' not in self.params[arg]: + self.params[arg]['paramType'] = 'query' + + def _parse_type(self, **kwargs): + arg = kwargs.get('arg', None) + body = self._get_body(**kwargs) + self.params.setdefault(arg, {}).update({ + 'name': arg, + 'dataType': body + }) + + def _parse_in(self, **kwargs): + arg = kwargs.get('arg', None) + body = self._get_body(**kwargs) + self.params.setdefault(arg, {}).update({ + 'name': arg, + 'paramType': body + }) + + def _parse_required(self, **kwargs): + arg = kwargs.get('arg', None) + body = self._get_body(**kwargs) + self.params.setdefault(arg, {}).update({ + 'name': arg, + 'required': False if body in ['False', 'false'] else True + }) + + def _parse_rtype(self, **kwargs): + body = self._get_body(**kwargs) + self.responseClass = body + + def _parse_property(self, **kwargs): + arg = kwargs.get('arg', None) + self.properties.setdefault(arg, {}).update({ + 'type': 'string' + }) + + def _parse_ptype(self, **kwargs): + arg = kwargs.get('arg', None) + code = self._parse_epytext_para('code', **kwargs) + link = self._parse_epytext_para('link', **kwargs) + if code is None: + self.properties.setdefault(arg, {}).update({ + 'type': link + }) + elif code == 'list': + self.properties.setdefault(arg, {}).update({ + 'type': 'array', + 'items': {'type': link} + }) + + def _parse_return(self, **kwargs): + arg = kwargs.get('arg', None) + body = self._get_body(**kwargs) + self.responseMessages.append({ + 'code': arg, + 'message': body + }) + + def _parse_notes(self, **kwargs): + body = self._get_body(**kwargs) + self.notes = self._sanitize_doc(body) + + def _parse_description(self, **kwargs): + body = self._get_body(**kwargs) + self.summary = self._sanitize_doc(body) + + def _not_supported(self, **kwargs): + pass + + @staticmethod + def _sanitize_doc(comment): + return comment.replace('\n', '
') if comment else comment + + @staticmethod + def _get_body(**kwargs): + body = kwargs.get('body', None) + return body.to_plaintext(None).strip() if body else body + + @staticmethod + def _parse_epytext_para(tag, **kwargs): + def _parse_epytext(tag, body): + epytextParser = EpytextParser(tag) + epytextParser.feed(str(body)) + data = epytextParser.get_data() + epytextParser.close() + return data + + body = kwargs.get('body', None) + return _parse_epytext(tag, body) if body else body + + +class model(DocParser): + def __init__(self, *args, **kwargs): + super(model, self).__init__() + self.args = args + self.kwargs = kwargs + self.required = [] + self.cls = None + + def __call__(self, *args): + if self.cls: + return self.cls + + cls = args[0] + self._parse_model(cls) + + return cls + + def _parse_model(self, cls): + self.id = cls.__name__ + self.cls = cls + if '__init__' in dir(cls): + self._parse_args(cls.__init__) + self.parse_docstring(inspect.getdoc(cls)) + settings.models.append(self) + + def _parse_args(self, func): + argspec = inspect.getargspec(func) + argspec.args.remove("self") + defaults = {} + if argspec.defaults: + defaults = list(zip(argspec.args[-len(argspec.defaults):], + argspec.defaults)) + required_args_count = len(argspec.args) - len(defaults) + for arg in argspec.args[:required_args_count]: + self.required.append(arg) + self.properties.setdefault(arg, {'type': 'string'}) + for arg, default in defaults: + self.properties.setdefault(arg, { + 'type': 'string', + "default": default + }) + + +class operation(DocParser): + def __init__(self, nickname='apis', **kwds): + super(operation, self).__init__() + self.nickname = nickname + self.func = None + self.func_args = [] + self.kwds = kwds + + def __call__(self, *args, **kwds): + if self.func: + return self.func(*args, **kwds) + + func = args[0] + self._parse_operation(func) + + @wraps(func) + def __wrapper__(*in_args, **in_kwds): + return self.func(*in_args, **in_kwds) + + __wrapper__.rest_api = self + return __wrapper__ + + def _parse_operation(self, func): + self.func = func + + self.__name__ = func.__name__ + self._parse_args(func) + self.parse_docstring(inspect.getdoc(self.func)) + + def _parse_args(self, func): + argspec = inspect.getargspec(func) + argspec.args.remove("self") + + defaults = [] + if argspec.defaults: + defaults = argspec.args[-len(argspec.defaults):] + + for arg in argspec.args: + if arg in defaults: + required = False + else: + required = True + self.params.setdefault(arg, { + 'name': arg, + 'required': required, + 'paramType': 'path', + 'dataType': 'string' + }) + self.func_args = argspec.args + + +def docs(**opts): + settings.docs_settings.update(opts) + + +class Application(tornado.web.Application): + def __init__(self, app_handlers=None, + default_host="", + transforms=None, + **settings): + super(Application, self).__init__( + handlers.swagger_handlers() + app_handlers, + default_host, + transforms, + **settings) diff --git a/opnfv_testapi/tornado_swagger/views.py b/opnfv_testapi/tornado_swagger/views.py new file mode 100644 index 0000000..7939997 --- /dev/null +++ b/opnfv_testapi/tornado_swagger/views.py @@ -0,0 +1,134 @@ +############################################################################## +# Copyright (c) 2016 ZTE Corporation +# feng.xiaowei@zte.com.cn +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +import inspect +import json + +import tornado.template +import tornado.web + +from opnfv_testapi.tornado_swagger import settings + + +def json_dumps(obj, pretty=False): + return json.dumps(obj, + sort_keys=True, + indent=4, + separators=(',', ': ')) if pretty else json.dumps(obj) + + +class SwaggerUIHandler(tornado.web.RequestHandler): + def initialize(self, **kwargs): + self.static_path = kwargs.get('static_path') + self.base_url = kwargs.get('base_url') + + def get_template_path(self): + return self.static_path + + def get(self): + resource_url = self.reverse_url(settings.RESOURCE_LISTING_NAME) + discovery_url = self.base_url + resource_url + self.render('swagger/index.html', discovery_url=discovery_url) + + +class SwaggerResourcesHandler(tornado.web.RequestHandler): + def initialize(self, **kwargs): + self.api_version = kwargs.get('api_version') + self.swagger_version = kwargs.get('swagger_version') + self.base_url = kwargs.get('base_url') + self.exclude_namespaces = kwargs.get('exclude_namespaces') + + def get(self): + self.set_header('content-type', 'application/json') + resources = { + 'apiVersion': self.api_version, + 'swaggerVersion': self.swagger_version, + 'basePath': self.base_url, + 'apis': [{ + 'path': self.reverse_url(settings.API_DECLARATION_NAME), + 'description': 'Restful APIs Specification' + }] + } + + self.finish(json_dumps(resources, self.get_arguments('pretty'))) + + +class SwaggerApiHandler(tornado.web.RequestHandler): + def initialize(self, **kwargs): + self.api_version = kwargs.get('api_version') + self.swagger_version = kwargs.get('swagger_version') + self.base_url = kwargs.get('base_url') + + def get(self): + self.set_header('content-type', 'application/json') + apis = self.find_api(self.application.handlers) + if apis is None: + raise tornado.web.HTTPError(404) + + specs = { + 'apiVersion': self.api_version, + 'swaggerVersion': self.swagger_version, + 'basePath': self.base_url, + 'resourcePath': '/', + 'produces': ["application/json"], + 'apis': [self.__get_api_spec__(path, spec, operations) + for path, spec, operations in apis], + 'models': self.__get_models_spec(settings.models) + } + self.finish(json_dumps(specs, self.get_arguments('pretty'))) + + def __get_models_spec(self, models): + models_spec = {} + for model in models: + models_spec.setdefault(model.id, self.__get_model_spec(model)) + return models_spec + + @staticmethod + def __get_model_spec(model): + return { + 'description': model.summary, + 'id': model.id, + 'notes': model.notes, + 'properties': model.properties, + 'required': model.required + } + + @staticmethod + def __get_api_spec__(path, spec, operations): + return { + 'path': path, + 'description': spec.handler_class.__doc__, + 'operations': [{ + 'httpMethod': api.func.__name__.upper(), + 'nickname': api.nickname, + 'parameters': api.params.values(), + 'summary': api.summary, + 'notes': api.notes, + 'responseClass': api.responseClass, + 'responseMessages': api.responseMessages, + } for api in operations] + } + + @staticmethod + def find_api(host_handlers): + def get_path(url, args): + return url % tuple(['{%s}' % arg for arg in args]) + + def get_operations(cls): + return [member.rest_api + for (_, member) in inspect.getmembers(cls) + if hasattr(member, 'rest_api')] + + for host, handlers in host_handlers: + for spec in handlers: + for (_, mbr) in inspect.getmembers(spec.handler_class): + if inspect.ismethod(mbr) and hasattr(mbr, 'rest_api'): + path = get_path(spec._path, mbr.rest_api.func_args) + operations = get_operations(spec.handler_class) + yield path, spec, operations + break diff --git a/opnfv_testapi/ui/__init__.py b/opnfv_testapi/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opnfv_testapi/ui/auth/__init__.py b/opnfv_testapi/ui/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opnfv_testapi/ui/auth/base.py b/opnfv_testapi/ui/auth/base.py new file mode 100644 index 0000000..bea87c4 --- /dev/null +++ b/opnfv_testapi/ui/auth/base.py @@ -0,0 +1,35 @@ +import random +import string + +from six.moves.urllib import parse + +from opnfv_testapi.resources import handlers + + +class BaseHandler(handlers.GenericApiHandler): + def __init__(self, application, request, **kwargs): + super(BaseHandler, self).__init__(application, request, **kwargs) + self.table = 'users' + + def set_cookies(self, cookies): + for cookie_n, cookie_v in cookies: + self.set_secure_cookie(cookie_n, cookie_v) + + +def get_token(length=30): + """Get random token.""" + return ''.join(random.choice(string.ascii_lowercase) + for i in range(length)) + + +def set_query_params(url, params): + """Set params in given query.""" + url_parts = parse.urlparse(url) + url = parse.urlunparse(( + url_parts.scheme, + url_parts.netloc, + url_parts.path, + url_parts.params, + parse.urlencode(params), + url_parts.fragment)) + return url diff --git a/opnfv_testapi/ui/auth/constants.py b/opnfv_testapi/ui/auth/constants.py new file mode 100644 index 0000000..44ccb46 --- /dev/null +++ b/opnfv_testapi/ui/auth/constants.py @@ -0,0 +1,18 @@ +OPENID = 'openid' +ROLE = 'role' +DEFAULT_ROLE = 'user' + +# OpenID parameters +OPENID_MODE = 'openid.mode' +OPENID_NS = 'openid.ns' +OPENID_RETURN_TO = 'openid.return_to' +OPENID_CLAIMED_ID = 'openid.claimed_id' +OPENID_IDENTITY = 'openid.identity' +OPENID_REALM = 'openid.realm' +OPENID_NS_SREG = 'openid.ns.sreg' +OPENID_NS_SREG_REQUIRED = 'openid.sreg.required' +OPENID_NS_SREG_EMAIL = 'openid.sreg.email' +OPENID_NS_SREG_FULLNAME = 'openid.sreg.fullname' +OPENID_ERROR = 'openid.error' + +CSRF_TOKEN = 'csrf_token' diff --git a/opnfv_testapi/ui/auth/jira_util.py b/opnfv_testapi/ui/auth/jira_util.py new file mode 100644 index 0000000..5ec91a7 --- /dev/null +++ b/opnfv_testapi/ui/auth/jira_util.py @@ -0,0 +1,66 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +import base64 +import os + +import oauth2 as oauth +from jira import JIRA +from tlslite.utils import keyfactory +from opnfv_testapi.common.config import CONF + + +class SignatureMethod_RSA_SHA1(oauth.SignatureMethod): + name = 'RSA-SHA1' + + def signing_base(self, request, consumer, token): + if not hasattr(request, 'normalized_url') or \ + request.normalized_url is None: + raise ValueError("Base URL for request is not set.") + + sig = ( + oauth.escape(request.method), + oauth.escape(request.normalized_url), + oauth.escape(request.get_normalized_parameters()), + ) + + key = '%s&' % oauth.escape(consumer.secret) + if token: + key += oauth.escape(token.secret) + raw = '&'.join(sig) + return key, raw + + def sign(self, request, consumer, token): + """Builds the base signature string.""" + key, raw = self.signing_base(request, consumer, token) + + module_dir = os.path.dirname(__file__) # get current directory + with open(module_dir + '/rsa.pem', 'r') as f: + data = f.read() + privateKeyString = data.strip() + privatekey = keyfactory.parsePrivateKey(privateKeyString) + raw = str.encode(raw) + signature = privatekey.hashAndSign(raw) + return base64.b64encode(signature) + + +def get_jira(access_token): + module_dir = os.path.dirname(__file__) # get current directory + with open(module_dir + '/rsa.pem', 'r') as f: + key_cert = f.read() + + oauth_dict = { + 'access_token': access_token['oauth_token'], + 'access_token_secret': access_token['oauth_token_secret'], + 'consumer_key': CONF.jira_oauth_consumer_key, + 'key_cert': key_cert + } + + return JIRA(server=CONF.jira_jira_url, oauth=oauth_dict) diff --git a/opnfv_testapi/ui/auth/rsa.pem b/opnfv_testapi/ui/auth/rsa.pem new file mode 100644 index 0000000..5ec1bbf --- /dev/null +++ b/opnfv_testapi/ui/auth/rsa.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAyDe0zz8Gpr1dh31c7R8LGV4p+wT0s2sUTtha1ex3GXzhEewQ +Cx3WUW/tttnBGd0oVVNMzoaIPbQJgPnuyVx2VugELLIdPHd9ngPDq09YWzK/J3VW +3I0sQLA5xVmGv32z4Uz7vbc7/4UM+FXPqUmhWBysR0zsm/nlLqbLs08GEyt2ZT+v +rNQnYtWA6YuL2OHSHvkQlwibpezHzs6LV7A8mQjWbptu0FL229h6pNSyV7YM459w +Z5RzBsPybl07P5HOVtkSizIFJ+HLc6yp4cVxjCgk4rMKyPBSG9dBEaHMlBCis61R +2lTbCJiy7SnWGiMd28WKuvu2k8T4A9k2FzvYwwIDAQABAoIBAGqhOFtTjqBIo8If +4tiqOsgE3UjBp+zR71vaX+4kZH2fg2J/HUA+YMC4YpqKOAwlO3DNz08CWRa7hoA5 +G5ID+0ZnhKmlJmronG8GRDQ9KqpPSXyjQmJtkQ7Wi73t4xSixqUL0dqE9qAr5O9x +DAp1m0cI5juG3VBoc0U4Ma5KPMsB3jceeV446ZsU07LSgTIOfLNzq6oEWLWhBzLj +rDRcGyB6iNxCsNacruW3DKrDg1cMqWqjxt6Tf4LuTWYFmGedTIktmn7VZDgXcbkK +a7sCRr7P0br6zuIFak1ugkUECDwNznLz3+QgW/iaay6NL6qEpnMLg3Z44kP+BLma +h5g/SvECgYEA7ewD4lG8s/iz5OIinVHIW10Bc8pEMX3+8cVCo+rq7YbWG+HqXFrv +DUcyRu/O3SHpc4ozkhRMTsVK5xGUuWGlLG9Hit5R4Ra8oHurJMsFUqjaptd9roHi +CMmynCFupqBwDoxMig5KxvuDqbOmo2yQOelP/UEnC+qlrux5+lClx4sCgYEA125H +KPAi30FkRJ/7pzlNtcqzNYQh6xdgcrDIsU1zHRa4AOPYSD+WSkb7wAbns5WLlOM8 +wScpUijyfu56YDizHuID4QW4ddKGVLEbx4tt8CiPLzweeFsP/FSfpd+OK0EDs8wP +S0b81rCkJKvGljfdl/wY3mYXOu0RZzXB55N1GqkCgYAscy+2lLbAmPJjDKyS37ii ++RlQXLWo2XVMDiKJJVaG0e4mf2qdno+S135ZKmxne/J1l5hS7l/jR5Da4rn6eHe3 +eYLQOwDpIKpVAUXUNenkq49OJGxisflc0vH/oW9eyhKlZSjXkhv+WPccOWgkmB/J +8gDzu7xjyY7yw1N2pKKUSQKBgQCfhdB5twALk698xX6igGNT10pGuZYoMEJCCzhB +WlmAU79jIVSZg0R1sgRfWH2gVH9se6wUVzxY02tlpI/HypSQrMo0iXji/kZsVk18 +wHljGZWVY44ojz3SGpOxT05GJzlnnRZCJsm47EpPwUcnGy0iixGbNbvD7aIya/Mu +2NkhKQKBgBgLvhfU3sU6XYrF99L63W1vcDyoXcsmQQtz2EzPflFkdcLYoeHo13XW +Apv7EeX+zqaeqx0v7xuVYWyde5ux9+vII4al0jToabLcd0y2k0Oxmjv40K1YVYsu +ZqoLXriNHf4NkqgQAFu8FfV1S9RTl6+3X4z6yzf09ustxiw3KWCz +-----END RSA PRIVATE KEY----- diff --git a/opnfv_testapi/ui/auth/sign.py b/opnfv_testapi/ui/auth/sign.py new file mode 100644 index 0000000..dbb40ed --- /dev/null +++ b/opnfv_testapi/ui/auth/sign.py @@ -0,0 +1,281 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from six.moves.urllib import parse +from tornado import gen +from tornado import web + +from cas import CASClient +from opnfv_testapi.ui.auth.jira_util import SignatureMethod_RSA_SHA1 +from opnfv_testapi.ui.auth.jira_util import get_jira + +from opnfv_testapi.common.config import CONF +from opnfv_testapi.db import api as dbapi +from opnfv_testapi.ui.auth import base +from opnfv_testapi.ui.auth import constants as const + +import logging +import oauth2 as oauth + +root = logging.getLogger() +root.setLevel(logging.DEBUG) + + +class SigninHandler(base.BaseHandler): + def get(self): + signin_type = self.get_query_argument("type") + self.set_secure_cookie("signin_type", signin_type) + if signin_type == "openstack": + self.signin_with_openstack() + if signin_type == "jira": + self.signin_with_jira() + if signin_type == "cas": + self.signin_with_cas() + + def signin_with_cas(self): + client = CASClient( + version='2', + renew=False, + extra_login_params=False, + server_url=CONF.lfid_url, + service_url=CONF.lfid_return_url + ) + redirect_url = client.get_login_url() + self.redirect(url=redirect_url, permanent=False) + + def signin_with_openstack(self): + csrf_token = base.get_token() + return_endpoint = parse.urljoin(CONF.api_url, + CONF.osid_openid_return_to) + return_to = base.set_query_params(return_endpoint, + {const.CSRF_TOKEN: csrf_token}) + + params = { + const.OPENID_MODE: CONF.osid_openid_mode, + const.OPENID_NS: CONF.osid_openid_ns, + const.OPENID_RETURN_TO: return_to, + const.OPENID_CLAIMED_ID: CONF.osid_openid_claimed_id, + const.OPENID_IDENTITY: CONF.osid_openid_identity, + const.OPENID_REALM: CONF.api_url, + const.OPENID_NS_SREG: CONF.osid_openid_ns_sreg, + const.OPENID_NS_SREG_REQUIRED: CONF.osid_openid_sreg_required, + } + url = CONF.osid_openstack_openid_endpoint + url = base.set_query_params(url, params) + self.redirect(url=url, permanent=False) + + def signin_with_jira(self): + consumer = oauth.Consumer(CONF.jira_oauth_consumer_key, + CONF.jira_oauth_consumer_secret) + client = oauth.Client(consumer) + client.set_signature_method(SignatureMethod_RSA_SHA1()) + + # Step 1. Get a request token from Jira. + try: + resp, content = client.request(CONF.jira_oauth_request_token_url, + "POST") + except Exception as e: + logging.error('Connect jira exception: %s', e) + self._auth_failure('Error: Connection to Jira failed. \ + Please contact an Administrator') + return + + if resp['status'] != '200': + logging.error('Connect jira error: %s', resp) + self._auth_failure('Error: Connection to Jira failed. \ + Error code(%s). \ + Please contact an Administrator' % (resp['status'])) + return + + # Step 2. Store the request token in a session for later use. + logging.warning('content is %s', content) + request_token = dict(parse.parse_qsl(content.decode())) + self.set_secure_cookie('oauth_token', request_token['oauth_token']) + self.set_secure_cookie('oauth_token_secret', + request_token['oauth_token_secret']) + + # Step 3. Redirect the user to the authentication URL. + url = CONF.jira_oauth_authorize_url + '?oauth_token=' + \ + request_token['oauth_token'] + \ + '&oauth_callback=' + CONF.jira_oauth_callback_url + self.redirect(url=url, permanent=False) + + def _auth_failure(self, message): + params = {'message': message} + url = parse.urljoin(CONF.ui_url, + '/#/auth_failure?' + parse.urlencode(params)) + self.redirect(url) + + +class SigninReturnHandler(base.BaseHandler): + @web.asynchronous + @gen.coroutine + def get(self): + if self.get_query_argument(const.OPENID_MODE) == 'cancel': + self._auth_failure('Authentication canceled.') + + openid = self.get_query_argument(const.OPENID_CLAIMED_ID) + role = const.DEFAULT_ROLE + new_user_info = { + 'openid': openid, + 'email': self.get_query_argument(const.OPENID_NS_SREG_EMAIL), + 'fullname': self.get_query_argument(const.OPENID_NS_SREG_FULLNAME), + const.ROLE: role + } + user = yield dbapi.db_find_one(self.table, {'openid': openid}) + if not user: + dbapi.db_save(self.table, new_user_info) + else: + role = user.get(const.ROLE) + + self.clear_cookie(const.OPENID) + self.clear_cookie(const.ROLE) + self.set_secure_cookie(const.OPENID, openid) + self.set_secure_cookie(const.ROLE, role) + self.redirect(url=CONF.ui_url) + + +class SigninReturnCasHandler(base.BaseHandler): + @web.asynchronous + @gen.coroutine + def get(self): + logging.warning("cas return") + ticket = self.get_query_argument('ticket') + logging.warning("ticket:%s", ticket) + client = CASClient( + version='2', + renew=False, + extra_login_params=False, + server_url=CONF.lfid_url, + service_url=CONF.lfid_return_url + ) + user, attrs, _ = client.verify_ticket(ticket) + logging.debug("user:%s", user) + logging.debug("attr:%s", attrs) + openid = user + role = const.DEFAULT_ROLE + new_user_info = { + 'openid': openid, + 'email': attrs['mail'], + 'fullname': attrs['profile_name_full'], + const.ROLE: role + } + user = yield dbapi.db_find_one(self.table, {'openid': openid}) + if not user: + dbapi.db_save(self.table, new_user_info) + else: + role = user.get(const.ROLE) + + self.clear_cookie(const.OPENID) + self.clear_cookie(const.ROLE) + self.clear_cookie('ticket') + self.set_secure_cookie(const.OPENID, openid) + self.set_secure_cookie(const.ROLE, role) + self.set_secure_cookie('ticket', ticket) + + self.redirect("/") + + +class SigninReturnJiraHandler(base.BaseHandler): + @web.asynchronous + @gen.coroutine + def get(self): + logging.warning("jira return") + # Step 1. Use the request token in the session to build a new client. + consumer = oauth.Consumer(CONF.jira_oauth_consumer_key, + CONF.jira_oauth_consumer_secret) + token = oauth.Token(self.get_secure_cookie('oauth_token'), + self.get_secure_cookie('oauth_token_secret')) + client = oauth.Client(consumer, token) + client.set_signature_method(SignatureMethod_RSA_SHA1()) + + # Step 2. Request the authorized access token from Jira. + try: + resp, content = client.request(CONF.jira_oauth_access_token_url, + "POST") + except Exception as e: + logging.error("Connect jira exception:%s", e) + self._auth_failure('Error: Connection to Jira failed. \ + Please contact an Administrator') + if resp['status'] != '200': + logging.error("Connect jira error:%s", resp) + self._auth_failure('Error: Connection to Jira failed. \ + Please contact an Administrator') + access_token = dict(parse.parse_qsl(content.decode())) + logging.warning("access_token: %s", access_token) + + # jira = JIRA(server=CONF.jira_jira_url, oauth=oauth_dict) + jira = get_jira(access_token) + lf_id = jira.current_user() + logging.warning("lf_id: %s", lf_id) + user = jira.myself() + logging.warning("user: %s", user) + # Step 3. Lookup the user or create them if they don't exist. + role = const.DEFAULT_ROLE + new_user_info = { + 'openid': lf_id, + 'email': user['emailAddress'], + 'fullname': user['displayName'], + const.ROLE: role + } + user = yield dbapi.db_find_one(self.table, {'openid': lf_id}) + if not user: + dbapi.db_save(self.table, new_user_info) + else: + role = user.get(const.ROLE) + + self.clear_cookie(const.OPENID) + self.clear_cookie(const.ROLE) + self.set_secure_cookie(const.OPENID, lf_id) + self.set_secure_cookie(const.ROLE, role) + self.redirect(url=CONF.ui_url) + + def _auth_failure(self, message): + params = {'message': message} + url = parse.urljoin(CONF.ui_url, + '/#/auth_failure?' + parse.urlencode(params)) + self.redirect(url) + + +class SignoutHandler(base.BaseHandler): + def get(self): + """Handle signout request.""" + self.clear_cookie(const.OPENID) + self.clear_cookie(const.ROLE) + signin_type = self.get_secure_cookie("signin_type") + if signin_type == "openstack": + self.signout_openstack() + if signin_type == "jira": + self.signout_jira() + if signin_type == 'cas': + self.signout_cas() + + def signout_openstack(self): + params = {'openid_logout': CONF.osid_openid_logout_endpoint} + url = parse.urljoin(CONF.ui_url, + '/#/logout?' + parse.urlencode(params)) + self.redirect(url) + + def signout_jira(self): + params = {'alt_token': ''} + url = parse.urljoin(CONF.jira_jira_url, + '/logout?' + parse.urlencode(params)) + self.redirect(url) + + def signout_cas(self): + client = CASClient( + version='2', + renew=False, + extra_login_params=False, + server_url=CONF.lfid_url, + service_url=CONF.lfid_return_url + ) + url = client.get_logout_url(CONF.ui_url) + self.redirect(url) diff --git a/opnfv_testapi/ui/auth/user.py b/opnfv_testapi/ui/auth/user.py new file mode 100644 index 0000000..a695da4 --- /dev/null +++ b/opnfv_testapi/ui/auth/user.py @@ -0,0 +1,35 @@ +############################################################################## +# Copyright (c) 2015 Orange +# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +from tornado import gen +from tornado import web + +from opnfv_testapi.common import raises +from opnfv_testapi.db import api as dbapi +from opnfv_testapi.ui.auth import base + + +class ProfileHandler(base.BaseHandler): + @web.asynchronous + @gen.coroutine + def get(self): + openid = self.get_secure_cookie('openid') + if openid: + try: + user = yield dbapi.db_find_one(self.table, {'openid': openid}) + self.finish_request({ + "openid": user.get('openid'), + "email": user.get('email'), + "fullname": user.get('fullname'), + "role": user.get('role', 'user'), + "type": self.get_secure_cookie('signin_type') + }) + except Exception: + pass + raises.Unauthorized('Unauthorized') diff --git a/opnfv_testapi/ui/root.py b/opnfv_testapi/ui/root.py new file mode 100644 index 0000000..5b2c922 --- /dev/null +++ b/opnfv_testapi/ui/root.py @@ -0,0 +1,10 @@ +from opnfv_testapi.resources.handlers import GenericApiHandler +from opnfv_testapi.common.config import CONF + + +class RootHandler(GenericApiHandler): + def get_template_path(self): + return CONF.static_path + + def get(self): + self.render('testapi-ui/index.html') -- cgit 1.2.3-korg