diff options
7 files changed, 863 insertions, 0 deletions
diff --git a/utils/test/result_collection_api/common/__init__.py b/utils/test/result_collection_api/common/__init__.py new file mode 100644 index 000000000..05c0c9392 --- /dev/null +++ b/utils/test/result_collection_api/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/utils/test/result_collection_api/common/config.py b/utils/test/result_collection_api/common/config.py new file mode 100644 index 000000000..9f7272be8 --- /dev/null +++ b/utils/test/result_collection_api/common/config.py @@ -0,0 +1,33 @@ +############################################################################## +# 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 ConfigParser import SafeConfigParser + +parser = SafeConfigParser() +parser.read('config.ini') + + +mongo_url = parser.get('default', 'mongo_url') +""" + + +def prepare_put_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 len(new_value) > 0: + if new_value != old_value: + edit_request[key] = new_value + + return edit_request diff --git a/utils/test/result_collection_api/common/constants.py b/utils/test/result_collection_api/common/constants.py new file mode 100644 index 000000000..485dbf314 --- /dev/null +++ b/utils/test/result_collection_api/common/constants.py @@ -0,0 +1,18 @@ +############################################################################## +# 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 +############################################################################## + + +API_LISTENING_PORT = 8000 + +MONGO_URL = "mongodb://192.168.56.102:27017/" + +APPLICATION_JSON = "application/json" +HTTP_BAD_REQUEST = 400 +HTTP_FORBIDDEN = 403 +HTTP_NOT_FOUND = 404 diff --git a/utils/test/result_collection_api/resources/__init__.py b/utils/test/result_collection_api/resources/__init__.py new file mode 100644 index 000000000..05c0c9392 --- /dev/null +++ b/utils/test/result_collection_api/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/utils/test/result_collection_api/resources/handlers.py b/utils/test/result_collection_api/resources/handlers.py new file mode 100644 index 000000000..64f75c287 --- /dev/null +++ b/utils/test/result_collection_api/resources/handlers.py @@ -0,0 +1,542 @@ +############################################################################## +# 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 json + +from tornado.web import RequestHandler, asynchronous, HTTPError +from tornado import gen +from datetime import datetime + +from models import Pod, TestProject, TestCase, TestResult +from common.constants import DEFAULT_REPRESENTATION, HTTP_BAD_REQUEST, \ + HTTP_NOT_FOUND, HTTP_FORBIDDEN +from common.config import prepare_put_request + + +class GenericApiHandler(RequestHandler): + """ + The purpose of this class is to take benefit of inheritance and prepare + a set of common functions for + the handlers + """ + + def initialize(self): + """ Prepares the database for the entire class """ + self.db = self.settings["db"] + + def prepare(self): + if not (self.request.method == "GET"): + if not (self.request.headers.get("Content-Type") is 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: + raise HTTPError(HTTP_BAD_REQUEST, + "Bad Json format [{}]". + format(error)) + else: + self.json_args = None + + def finish_request(self, json_object): + self.write(json.dumps(json_object)) + self.set_header("Content-Type", DEFAULT_REPRESENTATION) + self.finish() + + +class VersionHandler(RequestHandler): + """ Display a message for the API version """ + def get(self): + self.write("Collection of test result API, v1") + + +class PodHandler(GenericApiHandler): + """ Handle the requests about the POD Platforms + HTTP Methdods : + - GET : Get PODS + """ + + def initialize(self): + """ Prepares the database for the entire class """ + super(PodHandler, self).initialize() + + @asynchronous + @gen.coroutine + def get(self, pod_id=None): + """ + Get all pods or a single pod + :param pod_id: + """ + + if pod_id is None: + pod_id = "" + + get_request = dict() + + if len(pod_id) > 0: + get_request["_id"] = int(pod_id) + + res = [] + cursor = self.db.pod.find(get_request) + while (yield cursor.fetch_next): + pod = Pod.pod_from_dict(cursor.next_object()) + res.append(pod.format()) + + meta = dict() + meta["total"] = len(res) + meta["success"] = True if len(res) > 0 else False + + answer = dict() + answer["pods"] = res + answer["meta"] = meta + + self.finish_request(answer) + + +class TestProjectHandler(GenericApiHandler): + """ + TestProjectHandler Class + Handle the requests about the Test projects + HTTP Methdods : + - GET : Get all test projects and details about a specific one + - POST : Add a test project + - PUT : Edit test projects information (name and/or description) + - DELETE : Remove a test project + """ + + def initialize(self): + """ Prepares the database for the entire class """ + super(TestProjectHandler, self).initialize() + + @asynchronous + @gen.coroutine + def get(self, project_name=None): + """ + Get Project(s) info + :param project_name: + """ + + if project_name is None: + project_name = "" + + get_request = dict() + + if len(project_name) > 0: + get_request["name"] = project_name + + res = [] + cursor = self.db.test_projects.find(get_request) + while (yield cursor.fetch_next): + test_project = TestProject.testproject_from_dict( + cursor.next_object()) + res.append(test_project.format_http()) + + meta = dict() + meta["total"] = len(res) + meta["success"] = True if len(res) > 0 else False + + answer = dict() + answer["test_projects"] = res + answer["meta"] = meta + + self.finish_request(answer) + + @asynchronous + @gen.coroutine + def post(self): + """ Create a test project""" + + if self.json_args is None: + raise HTTPError(HTTP_BAD_REQUEST) + + query = {"name": self.json_args.get("name")} + + # check for name in db + mongo_dict = yield self.db.test_projects.find_one(query) + if not (mongo_dict is None): + raise HTTPError(HTTP_FORBIDDEN, + "{} already exists as a project".format( + self.json_args.get("name"))) + + test_project = TestProject.testproject_from_dict(self.json_args) + test_project.creation_date = datetime.now() + + future = self.db.test_projects.insert(test_project.format()) + result = yield future + test_project._id = result + + self.finish_request(test_project.format_http()) + + @asynchronous + @gen.coroutine + def put(self, project_name): + """ Updates the name and description of a test project""" + + print "PUT request for : {}".format(project_name) + + query = {'name': project_name} + mongo_dict = yield self.db.test_projects.find_one(query) + test_project = TestProject.testproject_from_dict(mongo_dict) + if test_project is None: + raise HTTPError(HTTP_NOT_FOUND, + "{} could not be found".format(project_name)) + + new_name = self.json_args.get("name") + new_description = self.json_args.get("description") + + # check for payload name parameter in db + # avoid a request if the project name has not changed in the payload + if new_name != test_project.name: + mongo_dict = yield self.db.test_projects.find_one( + {"name": new_name}) + if not (mongo_dict is None): + raise HTTPError(HTTP_FORBIDDEN, + "{} already exists as a project" + .format(new_name)) + + # new dict for changes + request = dict() + request = prepare_put_request(request, + "name", + new_name, + test_project.name) + request = prepare_put_request(request, + "description", + new_description, + test_project.description) + + """ raise exception if there isn't a change """ + if not request: + raise HTTPError(HTTP_FORBIDDEN, + "Nothing to update") + + """ we merge the whole document """ + edit_request = test_project.format() + edit_request.update(request) + + """ Updating the DB """ + res = yield self.db.test_projects.update({'name': project_name}, + edit_request) + print res + edit_request["_id"] = str(test_project._id) + + self.finish_request({"message": "success", "content": edit_request}) + + @asynchronous + @gen.coroutine + def delete(self, project_name): + """ Remove a test project""" + + print "DELETE request for : {}".format(project_name) + + # check for an existing case to be deleted + mongo_dict = yield self.db.test_cases.find_one( + {'project_name': project_name}) + test_project = TestProject.testproject_from_dict(mongo_dict) + if test_project is None: + raise HTTPError(HTTP_NOT_FOUND, + "{} could not be found as a project to be deleted" + .format(project_name)) + + # just delete it, or maybe save it elsewhere in a future + res = yield self.db.test_projects.remove( + {'project_name': project_name}) + print res + + self.finish_request({"message": "success"}) + + +class TestCasesHandler(GenericApiHandler): + """ + TestCasesHandler Class + Handle the requests about the Test cases for test projects + HTTP Methdods : + - GET : Get all test cases and details about a specific one + - POST : Add a test project + - PUT : Edit test projects information (name and/or description) + """ + + def initialize(self): + """ Prepares the database for the entire class """ + super(TestCasesHandler, self).initialize() + + @asynchronous + @gen.coroutine + def get(self, project_name, case_name=None): + """ + Get testcases(s) info + :param project_name: + :param case_name: + """ + + if case_name is None: + case_name = "" + + get_request = dict() + get_request["project_name"] = project_name + + if len(case_name) > 0: + get_request["name"] = case_name + + res = [] + cursor = self.db.test_cases.find(get_request) + print get_request + while (yield cursor.fetch_next): + test_case = TestCase.test_case_from_dict(cursor.next_object()) + res.append(test_case.format_http()) + + meta = dict() + meta["total"] = len(res) + meta["success"] = True if len(res) > 0 else False + + answer = dict() + answer["test_cases"] = res + answer["meta"] = meta + + self.finish_request(answer) + + @asynchronous + @gen.coroutine + def post(self, project_name): + """ Create a test case""" + + print "POST Request for {}".format(project_name) + + if self.json_args is None: + raise HTTPError(HTTP_BAD_REQUEST, + "Check your request payload") + + # retrieve test project + mongo_dict = yield self.db.test_projects.find_one( + {"name": project_name}) + if mongo_dict is None: + raise HTTPError(HTTP_FORBIDDEN, + "Could not find project {}" + .format(project_name)) + + # test_project = TestProject.testproject_from_dict(self.json_args) + + case = TestCase.test_case_from_dict(self.json_args) + case.project_name = project_name + case.creation_date = datetime.now() + + future = self.db.test_cases.insert(case.format()) + result = yield future + case._id = result + self.finish_request(case.format_http()) + + @asynchronous + @gen.coroutine + def put(self, project_name, case_name): + """ + Updates the name and description of a test case + :raises HTTPError (HTTP_NOT_FOUND, HTTP_FORBIDDEN) + """ + + print "PUT request for : {}/{}".format(project_name, case_name) + case_request = {'project_name': project_name, 'name': case_name} + + # check if there is a case for the project in url parameters + mongo_dict = yield self.db.test_cases.find_one(case_request) + test_case = TestCase.test_case_from_dict(mongo_dict) + if test_case is None: + raise HTTPError(HTTP_NOT_FOUND, + "{} could not be found as a {} case to be updated" + .format(case_name, project_name)) + + new_name = self.json_args.get("name") + new_project_name = self.json_args.get("project_name") + new_description = self.json_args.get("description") + + # check if there is not an existing test case + # with the name provided in the json payload + mongo_dict = yield self.db.test_cases.find_one( + {'project_name': new_project_name, 'name': new_name}) + if not (mongo_dict is None): + raise HTTPError(HTTP_FORBIDDEN, + "{} already exists as a project" + .format(new_name)) + + # new dict for changes + request = dict() + request = prepare_put_request(request, + "name", + new_name, + test_case.name) + request = prepare_put_request(request, + "project_name", + new_project_name, + test_case.project_name) + request = prepare_put_request(request, + "description", + new_description, + test_case.description) + + # we raise an exception if there isn't a change + if not request: + raise HTTPError(HTTP_FORBIDDEN, + "Nothing to update") + + # we merge the whole document """ + edit_request = test_case.format() + edit_request.update(request) + + """ Updating the DB """ + res = yield self.db.test_cases.update(case_request, edit_request) + print res + edit_request["_id"] = str(test_case._id) + + self.finish_request({"message": "success", "content": edit_request}) + + @asynchronous + @gen.coroutine + def delete(self, project_name, case_name): + """ Remove a test case""" + + print "DELETE request for : {}/{}".format(project_name, case_name) + case_request = {'project_name': project_name, 'name': case_name} + + # check for an existing case to be deleted + mongo_dict = yield self.db.test_cases.find_one(case_request) + test_project = TestProject.testproject_from_dict(mongo_dict) + if test_project is None: + raise HTTPError(HTTP_NOT_FOUND, + "{}/{} could not be found as a case to be deleted" + .format(project_name, case_name)) + + # just delete it, or maybe save it elsewhere in a future + res = yield self.db.test_projects.remove(case_request) + print res + + self.finish_request({"message": "success"}) + + +class TestResultsHandler(GenericApiHandler): + """ + TestResultsHandler Class + Handle the requests about the Test project's results + HTTP Methdods : + - GET : Get all test results and details about a specific one + - POST : Add a test results + - DELETE : Remove a test result + """ + + def initialize(self): + """ Prepares the database for the entire class """ + super(TestResultsHandler, self).initialize() + self.name = "test_result" + + @asynchronous + @gen.coroutine + def get(self, result_id=None): + """ + 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 ID + + :param result_id: Get a result by ID + :raise HTTPError + + GET /results/project=functest&case=keystone.catalog&pod=1 + => get results with optional filters + """ + + project_arg = self.get_query_argument("project", None) + case_arg = self.get_query_arguments("case", None) + pod_arg = self.get_query_arguments("pod", None) + + # prepare request + get_request = dict() + if result_id is None: + if not (project_arg is None): + get_request["project_name"] = project_arg + + if not (case_arg is None): + get_request["case_name"] = case_arg + + if not (pod_arg is None): + get_request["pod_id"] = pod_arg + else: + get_request["_id"] = result_id + + res = [] + # fetching results + cursor = self.db.test_cases.find(get_request) + while (yield cursor.fetch_next): + test_case = TestCase.test_case_from_dict(cursor.next_object) + res.append(test_case.format_http()) + + # building meta object + meta = dict() + meta["total"] = res.count() + + # final response object + answer = dict() + answer["test_results"] = res + answer["meta"] = meta + + self.finish_request(answer) + + @asynchronous + @gen.coroutine + def post(self): + """ + Create a new test result + :return: status of the request + :raise HTTPError + """ + + # check for request payload + if self.json_args is None: + raise HTTPError(HTTP_BAD_REQUEST) + + # check for missing parameters in the request payload + if self.json_args.get("project_name") is None: + raise HTTPError(HTTP_BAD_REQUEST) + if self.json_args.get("case_name") is None: + raise HTTPError(HTTP_BAD_REQUEST) + if self.json_args.get("pod_id") is None: + raise HTTPError(HTTP_BAD_REQUEST) + + # TODO : replace checks with jsonschema + # check for project + mongo_dict = yield self.db.test_projects.find_one( + {"name": self.json_args.get("project_name")}) + if not (mongo_dict is None): + raise HTTPError(HTTP_NOT_FOUND, + "Could not find project [{}] " + .format(self.json_args.get("project_name"))) + + # check for case + mongo_dict = yield self.db.test_cases.find_one( + {"name": self.json_args.get("case_name")}) + if not (mongo_dict is None): + raise HTTPError(HTTP_NOT_FOUND, + "Could not find case [{}] " + .format(self.json_args.get("case_name"))) + + # check for pod + mongo_dict = yield self.db.pod.find_one( + {"_id": self.json_args.get("pod_id")}) + if not (mongo_dict is None): + raise HTTPError(HTTP_NOT_FOUND, + "Could not find POD [{}] " + .format(self.json_args.get("pod_id"))) + + # convert payload to object + test_result = TestResult.test_result_from_dict(self.json_args) + test_result.creation_date = datetime.now() + + future = self.db.test_results.insert(test_result.format()) + result = yield future + test_result._id = result + + self.finish_request(test_result.format_http()) diff --git a/utils/test/result_collection_api/resources/models.py b/utils/test/result_collection_api/resources/models.py new file mode 100644 index 000000000..bb4cb0cf1 --- /dev/null +++ b/utils/test/result_collection_api/resources/models.py @@ -0,0 +1,171 @@ +############################################################################## +# 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 +############################################################################## + + +class Pod: + """ describes a POD platform """ + def __init__(self): + self._id = "" + self.name = "" + self.creation_date = "" + + @staticmethod + def pod_from_dict(pod_dict): + if pod_dict is None: + return None + + p = Pod() + p._id = pod_dict.get('_id') + p.creation_date = pod_dict.get('creation_date') + p.name = pod_dict.get('name') + return p + + def format(self): + return { + "_id": self._id, + "name": self.name, + "creation_date": str(self.creation_date), + } + + +class TestProject: + """ Describes a test project""" + + def __init__(self): + self._id = None + self.name = None + self.description = None + self.creation_date = None + + @staticmethod + def testproject_from_dict(testproject_dict): + + if testproject_dict is None: + return None + + t = TestProject() + t._id = testproject_dict.get('_id') + t.creation_date = testproject_dict.get('creation_date') + t.name = testproject_dict.get('name') + t.description = testproject_dict.get('description') + + return t + + def format(self): + return { + "name": self.name, + "description": self.description, + "creation_date": str(self.creation_date) + } + + def format_http(self, test_cases=0): + return { + "_id": str(self._id), + "name": self.name, + "description": self.description, + "creation_date": str(self.creation_date), + "test_cases": test_cases + } + + +class TestCase: + """ Describes a test case""" + + def __init__(self): + self._id = None + self.name = None + self.project_name = None + self.description = None + self.creation_date = None + + @staticmethod + def test_case_from_dict(testcase_dict): + + if testcase_dict is None: + return None + + t = TestCase() + t._id = testcase_dict.get('_id') + t.project_name = testcase_dict.get('project_name') + t.creation_date = testcase_dict.get('creation_date') + t.name = testcase_dict.get('name') + t.description = testcase_dict.get('description') + + return t + + def format(self): + return { + "name": self.name, + "description": self.description, + "project_name": self.project_name, + "creation_date": str(self.creation_date) + } + + def format_http(self, test_project=None): + res = { + "_id": str(self._id), + "name": self.name, + "description": self.description, + "creation_date": str(self.creation_date), + } + if not (test_project is None): + res["test_project"] = test_project + + return res + + +class TestResult: + """ Describes a test result""" + + def __init__(self): + self._id = None + self.case_name = None + self.project_name = None + self.pod_id = None + self.description = None + self.creation_date = None + self.details = None + + @staticmethod + def test_result_from_dict(test_result_dict): + + if test_result_dict is None: + return None + + t = TestResult() + t._id = test_result_dict.get('_id') + t.case_name = test_result_dict.get('case_name') + t.project_name = test_result_dict.get('project_name') + t.pod_id = test_result_dict.get('pod_id') + t.description = test_result_dict.get('description') + t.creation_date = str(test_result_dict.get('creation_date')) + t.details = test_result_dict.get('details') + + return t + + def format(self): + return { + "case_name": self.case_name, + "project_name": self.project_name, + "pod_id": self.pod_id, + "description": self.description, + "creation_date": self.creation_date, + "details": self.details, + } + + def format_http(self): + return { + "_id": str(self._id), + "case_name": self.case_name, + "project_name": self.project_name, + "pod_id": self.pod_id, + "description": self.description, + "creation_date": self.creation_date, + "details": self.details, + } diff --git a/utils/test/result_collection_api/result_collection_api.py b/utils/test/result_collection_api/result_collection_api.py new file mode 100644 index 000000000..71b3267c2 --- /dev/null +++ b/utils/test/result_collection_api/result_collection_api.py @@ -0,0 +1,83 @@ +############################################################################## +# 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 : + - json args validation with schemes + - count cases for GET on test_projects + - count results for GET on cases + - add meta object to json response + - provide filtering on requests + - include objects + +""" + +import tornado.ioloop +import motor + +from resources.handlers import VersionHandler, PodHandler, \ + TestProjectHandler, TestCasesHandler, TestResultsHandler +from common.constants import API_LISTENING_PORT, MONGO_URL + +# connecting to MongoDB server, and choosing database +db = motor.MotorClient(MONGO_URL).test_results_collection + + +def make_app(): + return tornado.web.Application( + [ + # GET /version => GET API version + (r"/version", VersionHandler), + + # few examples: + # GET /pods => Get all pods + # GET /pods/1 => Get details on POD 1 + (r"/pods", PodHandler), + (r"/pods/(\d*)", PodHandler), + + # few examples: + # GET /test_projects + # GET /test_projects/yardstick + (r"/test_projects", TestProjectHandler), + (r"/test_projects/([^/]+)", TestProjectHandler), + + # few examples + # GET /test_projects/qtip/cases => Get cases for qtip + # + (r"/test_projects/([^/]+)/cases", TestCasesHandler), + (r"/test_projects/([^/]+)/cases/([^/]+)", TestCasesHandler), + # (r"/test_cases/([^/]+)", TestCasesHandler), + + # new path to avoid a long depth + # GET /results?project=functest&case=keystone.catalog&pod=1 + # => get results with optional filters + # POST /results => + # Push results with mandatory request payload parameters + # (project, case, and pod_id) + (r"/results([^/]*)", TestResultsHandler), + (r"/results/([^/]*)", TestResultsHandler), + ], + db=db, + debug=True, + ) + + +def main(): + application = make_app() + application.listen(API_LISTENING_PORT) + tornado.ioloop.IOLoop.current().start() + +if __name__ == "__main__": + main() |