diff options
author | hongbo tian <hongbo.tianhongbo@huawei.com> | 2017-09-28 09:28:41 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@opnfv.org> | 2017-09-28 09:28:41 +0000 |
commit | 61820ee85c967d90021e4089c6a7907046685639 (patch) | |
tree | 0c3f23e2146f0855fb5be0ff55343d59e3a9c9ab /cvp/opnfv_testapi | |
parent | 0cc2fdefa9e6e959e7c69beede736738f339f636 (diff) | |
parent | 0cf6b232ac9cf128ee9183a27c08f4f74ab2e2e6 (diff) |
Merge "add api&web services for cvp"
Diffstat (limited to 'cvp/opnfv_testapi')
68 files changed, 6221 insertions, 0 deletions
diff --git a/cvp/opnfv_testapi/__init__.py b/cvp/opnfv_testapi/__init__.py new file mode 100644 index 00000000..363bc388 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/cmd/__init__.py b/cvp/opnfv_testapi/cmd/__init__.py new file mode 100644 index 00000000..363bc388 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/cmd/server.py b/cvp/opnfv_testapi/cmd/server.py new file mode 100644 index 00000000..a5ac5eb6 --- /dev/null +++ b/cvp/opnfv_testapi/cmd/server.py @@ -0,0 +1,57 @@ +############################################################################## +# 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 + +from opnfv_testapi.common.config import CONF +from opnfv_testapi.router import url_mappings +from opnfv_testapi.tornado_swagger import swagger + + +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/cvp/opnfv_testapi/common/__init__.py b/cvp/opnfv_testapi/common/__init__.py new file mode 100644 index 00000000..05c0c939 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/common/check.py b/cvp/opnfv_testapi/common/check.py new file mode 100644 index 00000000..24ba876a --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/common/config.py b/cvp/opnfv_testapi/common/config.py new file mode 100644 index 00000000..75dbc35d --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/common/message.py b/cvp/opnfv_testapi/common/message.py new file mode 100644 index 00000000..61ce03dc --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/common/raises.py b/cvp/opnfv_testapi/common/raises.py new file mode 100644 index 00000000..ec6b8a56 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/db/__init__.py b/cvp/opnfv_testapi/db/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/cvp/opnfv_testapi/db/__init__.py diff --git a/cvp/opnfv_testapi/db/api.py b/cvp/opnfv_testapi/db/api.py new file mode 100644 index 00000000..c057480d --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/__init__.py b/cvp/opnfv_testapi/resources/__init__.py new file mode 100644 index 00000000..05c0c939 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/application_handlers.py b/cvp/opnfv_testapi/resources/application_handlers.py new file mode 100644 index 00000000..16dae4f3 --- /dev/null +++ b/cvp/opnfv_testapi/resources/application_handlers.py @@ -0,0 +1,147 @@ +############################################################################## +# 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.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 ApplicationsCLHandler(GenericApplicationHandler): + @swagger.operation(nickname="queryApplications") + 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 + } + + self._list(query=self.set_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) + + +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/cvp/opnfv_testapi/resources/application_models.py b/cvp/opnfv_testapi/resources/application_models.py new file mode 100644 index 00000000..e2bb652d --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/handlers.py b/cvp/opnfv_testapi/resources/handlers.py new file mode 100644 index 00000000..17a7e359 --- /dev/null +++ b/cvp/opnfv_testapi/resources/handlers.py @@ -0,0 +1,321 @@ +############################################################################## +# 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 + + 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) + role = self.get_secure_cookie(auth_const.ROLE) + logging.info('role:%s', role) + if role: + if role.find("reviewer") != -1: + query['$or'] = [{"shared": + {"$elemMatch": {"$eq": openid}} + }, {"owner": openid}, + {"status": {"$ne": "private"}}] + else: + query['$or'] = [{"shared": + {"$elemMatch": {"$eq": openid}} + }, {"owner": openid}] + 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) + 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): + 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/cvp/opnfv_testapi/resources/models.py b/cvp/opnfv_testapi/resources/models.py new file mode 100644 index 00000000..e8fc532b --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/pod_handlers.py b/cvp/opnfv_testapi/resources/pod_handlers.py new file mode 100644 index 00000000..50298875 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/pod_models.py b/cvp/opnfv_testapi/resources/pod_models.py new file mode 100644 index 00000000..2c3ea978 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/project_handlers.py b/cvp/opnfv_testapi/resources/project_handlers.py new file mode 100644 index 00000000..be295070 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/project_models.py b/cvp/opnfv_testapi/resources/project_models.py new file mode 100644 index 00000000..3243882b --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/result_handlers.py b/cvp/opnfv_testapi/resources/result_handlers.py new file mode 100644 index 00000000..8cb9a347 --- /dev/null +++ b/cvp/opnfv_testapi/resources/result_handlers.py @@ -0,0 +1,303 @@ +############################################################################## +# 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") + results = tar_in.extractfile('results/results.json').read() + 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/cvp/opnfv_testapi/resources/result_models.py b/cvp/opnfv_testapi/resources/result_models.py new file mode 100644 index 00000000..698f4981 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/scenario_handlers.py b/cvp/opnfv_testapi/resources/scenario_handlers.py new file mode 100644 index 00000000..5d420a56 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/scenario_models.py b/cvp/opnfv_testapi/resources/scenario_models.py new file mode 100644 index 00000000..467cff24 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/test_handlers.py b/cvp/opnfv_testapi/resources/test_handlers.py new file mode 100644 index 00000000..9ad1bbc2 --- /dev/null +++ b/cvp/opnfv_testapi/resources/test_handlers.py @@ -0,0 +1,200 @@ +############################################################################## +# 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 message +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 + + +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") + 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 + } + + self._list(query=self.set_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") + def get(self, test_id): + query = dict() + query["_id"] = objectid.ObjectId(test_id) + self._get_one(query=query) + + @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, test_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(test_id, item, value) + except Exception as e: + logging.error('except:%s', e) + return + + @gen.coroutine + def update(self, test_id, item, value): + logging.debug("update") + if item == "shared": + if len(value) != len(set(value)): + msg = "Already shared with this user" + self.finish_request({'code': '403', 'msg': msg}) + return + + for user in value: + query = {"openid": 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 + + 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': test_id} + db_keys = ['id', ] + user = self.get_secure_cookie(auth_const.OPENID) + if item == "shared": + query['owner'] = 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/cvp/opnfv_testapi/resources/test_models.py b/cvp/opnfv_testapi/resources/test_models.py new file mode 100644 index 00000000..41fa6852 --- /dev/null +++ b/cvp/opnfv_testapi/resources/test_models.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 +############################################################################## +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=[], 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 + + +@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/cvp/opnfv_testapi/resources/testcase_handlers.py b/cvp/opnfv_testapi/resources/testcase_handlers.py new file mode 100644 index 00000000..9399326f --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/resources/testcase_models.py b/cvp/opnfv_testapi/resources/testcase_models.py new file mode 100644 index 00000000..2379dfc4 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/router/__init__.py b/cvp/opnfv_testapi/router/__init__.py new file mode 100644 index 00000000..3fc79f1d --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/router/url_mappings.py b/cvp/opnfv_testapi/router/url_mappings.py new file mode 100644 index 00000000..73c771fe --- /dev/null +++ b/cvp/opnfv_testapi/router/url_mappings.py @@ -0,0 +1,39 @@ +############################################################################## +# 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.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", application_handlers.ApplicationsCLHandler), + (r"/api/v1/cvp/applications/([^/]+)", + application_handlers.ApplicationsGURHandler), + + + + (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/cvp/opnfv_testapi/tests/__init__.py b/cvp/opnfv_testapi/tests/__init__.py new file mode 100644 index 00000000..9f28b0bf --- /dev/null +++ b/cvp/opnfv_testapi/tests/__init__.py @@ -0,0 +1 @@ +__author__ = 'serena' diff --git a/cvp/opnfv_testapi/tests/unit/__init__.py b/cvp/opnfv_testapi/tests/unit/__init__.py new file mode 100644 index 00000000..3fc79f1d --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/common/__init__.py b/cvp/opnfv_testapi/tests/unit/common/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/cvp/opnfv_testapi/tests/unit/common/__init__.py diff --git a/cvp/opnfv_testapi/tests/unit/common/noparam.ini b/cvp/opnfv_testapi/tests/unit/common/noparam.ini new file mode 100644 index 00000000..fda2a09e --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/common/normal.ini b/cvp/opnfv_testapi/tests/unit/common/normal.ini new file mode 100644 index 00000000..77cc6c6e --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/common/nosection.ini b/cvp/opnfv_testapi/tests/unit/common/nosection.ini new file mode 100644 index 00000000..9988fc0a --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/common/notboolean.ini b/cvp/opnfv_testapi/tests/unit/common/notboolean.ini new file mode 100644 index 00000000..b3f32767 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/common/notint.ini b/cvp/opnfv_testapi/tests/unit/common/notint.ini new file mode 100644 index 00000000..d1b752a3 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/common/test_config.py b/cvp/opnfv_testapi/tests/unit/common/test_config.py new file mode 100644 index 00000000..cc8743ca --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/conftest.py b/cvp/opnfv_testapi/tests/unit/conftest.py new file mode 100644 index 00000000..feff1daa --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/executor.py b/cvp/opnfv_testapi/tests/unit/executor.py new file mode 100644 index 00000000..b8f696ca --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/fake_pymongo.py b/cvp/opnfv_testapi/tests/unit/fake_pymongo.py new file mode 100644 index 00000000..0ca83df6 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/resources/__init__.py b/cvp/opnfv_testapi/tests/unit/resources/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/cvp/opnfv_testapi/tests/unit/resources/__init__.py diff --git a/cvp/opnfv_testapi/tests/unit/resources/scenario-c1.json b/cvp/opnfv_testapi/tests/unit/resources/scenario-c1.json new file mode 100644 index 00000000..18780221 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/resources/scenario-c2.json b/cvp/opnfv_testapi/tests/unit/resources/scenario-c2.json new file mode 100644 index 00000000..b6a3b83a --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/resources/test_base.py b/cvp/opnfv_testapi/tests/unit/resources/test_base.py new file mode 100644 index 00000000..dcec4e95 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/resources/test_fake_pymongo.py b/cvp/opnfv_testapi/tests/unit/resources/test_fake_pymongo.py new file mode 100644 index 00000000..1ebc96f3 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/resources/test_pod.py b/cvp/opnfv_testapi/tests/unit/resources/test_pod.py new file mode 100644 index 00000000..cb4f1d92 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/resources/test_project.py b/cvp/opnfv_testapi/tests/unit/resources/test_project.py new file mode 100644 index 00000000..0622ba8d --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/resources/test_result.py b/cvp/opnfv_testapi/tests/unit/resources/test_result.py new file mode 100644 index 00000000..1e83ed30 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/resources/test_scenario.py b/cvp/opnfv_testapi/tests/unit/resources/test_scenario.py new file mode 100644 index 00000000..bd720671 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/resources/test_testcase.py b/cvp/opnfv_testapi/tests/unit/resources/test_testcase.py new file mode 100644 index 00000000..4f2bc2ad --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/resources/test_token.py b/cvp/opnfv_testapi/tests/unit/resources/test_token.py new file mode 100644 index 00000000..940e256c --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tests/unit/resources/test_version.py b/cvp/opnfv_testapi/tests/unit/resources/test_version.py new file mode 100644 index 00000000..51fed11e --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tornado_swagger/README.md b/cvp/opnfv_testapi/tornado_swagger/README.md new file mode 100644 index 00000000..d815f216 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tornado_swagger/__init__.py b/cvp/opnfv_testapi/tornado_swagger/__init__.py new file mode 100644 index 00000000..363bc388 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tornado_swagger/handlers.py b/cvp/opnfv_testapi/tornado_swagger/handlers.py new file mode 100644 index 00000000..e39a9f63 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tornado_swagger/settings.py b/cvp/opnfv_testapi/tornado_swagger/settings.py new file mode 100644 index 00000000..28422611 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/tornado_swagger/swagger.py b/cvp/opnfv_testapi/tornado_swagger/swagger.py new file mode 100644 index 00000000..83f389a6 --- /dev/null +++ b/cvp/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', '<br/>') 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/cvp/opnfv_testapi/tornado_swagger/views.py b/cvp/opnfv_testapi/tornado_swagger/views.py new file mode 100644 index 00000000..79399970 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/ui/__init__.py b/cvp/opnfv_testapi/ui/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/cvp/opnfv_testapi/ui/__init__.py diff --git a/cvp/opnfv_testapi/ui/auth/__init__.py b/cvp/opnfv_testapi/ui/auth/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/cvp/opnfv_testapi/ui/auth/__init__.py diff --git a/cvp/opnfv_testapi/ui/auth/base.py b/cvp/opnfv_testapi/ui/auth/base.py new file mode 100644 index 00000000..bea87c4d --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/ui/auth/constants.py b/cvp/opnfv_testapi/ui/auth/constants.py new file mode 100644 index 00000000..44ccb46d --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/ui/auth/jira_util.py b/cvp/opnfv_testapi/ui/auth/jira_util.py new file mode 100644 index 00000000..5ec91a71 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/ui/auth/rsa.pem b/cvp/opnfv_testapi/ui/auth/rsa.pem new file mode 100644 index 00000000..5ec1bbf1 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/ui/auth/sign.py b/cvp/opnfv_testapi/ui/auth/sign.py new file mode 100644 index 00000000..dbb40ed0 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/ui/auth/user.py b/cvp/opnfv_testapi/ui/auth/user.py new file mode 100644 index 00000000..a695da45 --- /dev/null +++ b/cvp/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/cvp/opnfv_testapi/ui/root.py b/cvp/opnfv_testapi/ui/root.py new file mode 100644 index 00000000..5b2c922d --- /dev/null +++ b/cvp/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') |