1 ##############################################################################
2 # Copyright (c) 2015 Orange
3 # guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 # feng.xiaowei@zte.com.cn refactor db.pod to db.pods 5-19-2016
9 # feng.xiaowei@zte.com.cn refactor test_project to project 5-19-2016
10 # feng.xiaowei@zte.com.cn refactor response body 5-19-2016
11 # feng.xiaowei@zte.com.cn refactor pod/project response info 5-19-2016
12 # feng.xiaowei@zte.com.cn refactor case related handler 5-20-2016
13 ##############################################################################
17 from tornado.web import RequestHandler, asynchronous, HTTPError
18 from tornado import gen
19 from datetime import datetime, timedelta
21 from models import TestResult, CreateResponse
22 from resources.testcase_models import Testcase
23 from resources.project_models import Project
24 from resources.pod_models import Pod
25 from common.constants import DEFAULT_REPRESENTATION, HTTP_BAD_REQUEST, \
26 HTTP_NOT_FOUND, HTTP_FORBIDDEN
27 from common.config import prepare_put_request
29 from dashboard.dashboard_utils import get_dashboard_cases, \
30 check_dashboard_ready_project, check_dashboard_ready_case, \
34 def format_data(data, cls):
35 cls_data = cls.from_dict(data)
36 return cls_data.format_http()
39 class GenericApiHandler(RequestHandler):
41 The purpose of this class is to take benefit of inheritance and prepare
42 a set of common functions for
47 """ Prepares the database for the entire class """
48 self.db = self.settings["db"]
51 if not (self.request.method == "GET" or self.request.method == "DELETE"):
52 if self.request.headers.get("Content-Type") is not None:
53 if self.request.headers["Content-Type"].startswith(
54 DEFAULT_REPRESENTATION):
56 self.json_args = json.loads(self.request.body)
57 except (ValueError, KeyError, TypeError) as error:
58 raise HTTPError(HTTP_BAD_REQUEST,
59 "Bad Json format [{}]".
64 def finish_request(self, json_object=None):
66 self.write(json.dumps(json_object))
67 self.set_header("Content-Type", DEFAULT_REPRESENTATION)
71 class VersionHandler(RequestHandler):
72 """ Display a message for the API version """
74 self.write("Collection of test result API, v1")
77 class PodHandler(GenericApiHandler):
78 """ Handle the requests about the POD Platforms
86 """ Prepares the database for the entire class """
87 super(PodHandler, self).initialize()
91 def get(self, pod_name=None):
93 Get all pods or a single pod
98 if pod_name is not None:
99 query["name"] = pod_name
100 answer = yield self.db.pods.find_one(query)
102 raise HTTPError(HTTP_NOT_FOUND,
103 "{} Not Exist".format(pod_name))
105 answer = format_data(answer, Pod)
108 cursor = self.db.pods.find(query)
109 while (yield cursor.fetch_next):
110 res.append(format_data(cursor.next_object(), Pod))
111 answer = {'pods': res}
113 self.finish_request(answer)
120 if self.json_args is None:
121 raise HTTPError(HTTP_BAD_REQUEST)
123 query = {"name": self.json_args.get("name")}
125 # check for existing name in db
126 the_pod = yield self.db.pods.find_one(query)
127 if the_pod is not None:
128 raise HTTPError(HTTP_FORBIDDEN,
129 "{} already exists as a pod".format(
130 self.json_args.get("name")))
132 pod = Pod.from_dict(self.json_args)
133 pod.creation_date = datetime.now()
135 yield self.db.pods.insert(pod.format())
137 res = CreateResponse(self.request.full_url() + '/{}'.format(pod.name))
138 self.finish_request(res.format())
142 def delete(self, pod_name):
145 # check for an existing pod to be deleted
146 mongo_dict = yield self.db.pods.find_one(
148 pod = TestProject.pod(mongo_dict)
150 raise HTTPError(HTTP_NOT_FOUND,
151 "{} could not be found as a pod to be deleted"
154 # just delete it, or maybe save it elsewhere in a future
155 res = yield self.db.projects.remove(
158 self.finish_request(answer)
163 class ProjectHandler(GenericApiHandler):
165 TestProjectHandler Class
166 Handle the requests about the Test projects
168 - GET : Get all test projects and details about a specific one
169 - POST : Add a test project
170 - PUT : Edit test projects information (name and/or description)
171 - DELETE : Remove a test project
174 def initialize(self):
175 """ Prepares the database for the entire class """
176 super(ProjectHandler, self).initialize()
180 def get(self, project_name=None):
188 if project_name is not None:
189 query["name"] = project_name
190 answer = yield self.db.projects.find_one(query)
192 raise HTTPError(HTTP_NOT_FOUND,
193 "{} Not Exist".format(project_name))
195 answer = format_data(answer, Project)
198 cursor = self.db.projects.find(query)
199 while (yield cursor.fetch_next):
200 res.append(format_data(cursor.next_object(), Project))
201 answer = {'projects': res}
203 self.finish_request(answer)
208 """ Create a test project"""
210 if self.json_args is None:
211 raise HTTPError(HTTP_BAD_REQUEST)
213 query = {"name": self.json_args.get("name")}
215 # check for name in db
216 the_project = yield self.db.projects.find_one(query)
217 if the_project is not None:
218 raise HTTPError(HTTP_FORBIDDEN,
219 "{} already exists as a project".format(
220 self.json_args.get("name")))
222 project = Project.from_dict(self.json_args)
223 project.creation_date = datetime.now()
225 yield self.db.projects.insert(project.format())
227 res = CreateResponse(self.request.full_url() + '/{}'.format(project.name))
228 self.finish_request(res.format())
233 def put(self, project_name):
234 """ Updates the name and description of a test project"""
236 if self.json_args is None:
237 raise HTTPError(HTTP_BAD_REQUEST)
239 query = {'name': project_name}
240 from_project = yield self.db.projects.find_one(query)
241 if from_project is None:
242 raise HTTPError(HTTP_NOT_FOUND,
243 "{} could not be found".format(project_name))
245 project = Project.from_dict(from_project)
246 new_name = self.json_args.get("name")
247 new_description = self.json_args.get("description")
249 # check for payload name parameter in db
250 # avoid a request if the project name has not changed in the payload
251 if new_name != project.name:
252 to_project = yield self.db.projects.find_one(
254 if to_project is not None:
255 raise HTTPError(HTTP_FORBIDDEN,
256 "{} already exists as a project"
259 # new dict for changes
261 request = prepare_put_request(request,
265 request = prepare_put_request(request,
270 """ raise exception if there isn't a change """
272 raise HTTPError(HTTP_FORBIDDEN, "Nothing to update")
274 """ we merge the whole document """
275 edit_request = project.format()
276 edit_request.update(request)
278 """ Updating the DB """
279 yield self.db.projects.update({'name': project_name}, edit_request)
280 new_project = yield self.db.projects.find_one({"_id": project._id})
282 self.finish_request(format_data(new_project, Project))
286 def delete(self, project_name):
287 """ Remove a test project"""
288 query = {'name': project_name}
290 # check for an existing project to be deleted
291 project = yield self.db.projects.find_one(query)
293 raise HTTPError(HTTP_NOT_FOUND,
294 "{} could not be found as a project to be deleted"
295 .format(project_name))
297 # just delete it, or maybe save it elsewhere in a future
298 yield self.db.projects.remove(query)
300 self.finish_request()
303 class TestcaseHandler(GenericApiHandler):
305 TestCasesHandler Class
306 Handle the requests about the Test cases for test projects
308 - GET : Get all test cases and details about a specific one
309 - POST : Add a test project
310 - PUT : Edit test projects information (name and/or description)
313 def initialize(self):
314 """ Prepares the database for the entire class """
315 super(TestcaseHandler, self).initialize()
319 def get(self, project_name, case_name=None):
321 Get testcases(s) info
326 query = {'project_name': project_name}
328 if case_name is not None:
329 query["name"] = case_name
330 answer = yield self.db.testcases.find_one(query)
332 raise HTTPError(HTTP_NOT_FOUND,
333 "{} Not Exist".format(case_name))
335 answer = format_data(answer, Testcase)
338 cursor = self.db.testcases.find(query)
339 while (yield cursor.fetch_next):
340 res.append(format_data(cursor.next_object(), Testcase))
341 answer = {'testcases': res}
343 self.finish_request(answer)
347 def post(self, project_name):
348 """ Create a test case"""
350 if self.json_args is None:
351 raise HTTPError(HTTP_BAD_REQUEST,
352 "Check your request payload")
354 # retrieve test project
355 project = yield self.db.projects.find_one(
356 {"name": project_name})
358 raise HTTPError(HTTP_FORBIDDEN,
359 "Could not find project {}"
360 .format(project_name))
362 case_name = self.json_args.get('name')
363 the_testcase = yield self.db.testcases.find_one(
364 {"project_name": project_name, 'name': case_name})
366 raise HTTPError(HTTP_FORBIDDEN,
367 "{} already exists as a case in project {}"
368 .format(case_name, project_name))
370 testcase = Testcase.from_dict(self.json_args)
371 testcase.project_name = project_name
372 testcase.creation_date = datetime.now()
374 yield self.db.testcases.insert(testcase.format())
375 res = CreateResponse(self.request.full_url() + '/{}'.format(testcase.name))
376 self.finish_request(res.format())
380 def put(self, project_name, case_name):
382 Updates the name and description of a test case
383 :raises HTTPError (HTTP_NOT_FOUND, HTTP_FORBIDDEN)
386 query = {'project_name': project_name, 'name': case_name}
388 if self.json_args is None:
389 raise HTTPError(HTTP_BAD_REQUEST, "No payload")
391 # check if there is a case for the project in url parameters
392 from_testcase = yield self.db.testcases.find_one(query)
393 if from_testcase is None:
394 raise HTTPError(HTTP_NOT_FOUND,
395 "{} could not be found as a {} case to be updated"
396 .format(case_name, project_name))
398 testcase = Testcase.from_dict(from_testcase)
399 new_name = self.json_args.get("name")
400 new_project_name = self.json_args.get("project_name")
401 if not new_project_name:
402 new_project_name = project_name
403 new_description = self.json_args.get("description")
405 # check if there is not an existing test case
406 # with the name provided in the json payload
407 if new_name != case_name or new_project_name != project_name:
408 to_testcase = yield self.db.testcases.find_one(
409 {'project_name': new_project_name, 'name': new_name})
410 if to_testcase is not None:
411 raise HTTPError(HTTP_FORBIDDEN,
412 "{} already exists as a case in project"
413 .format(new_name, new_project_name))
415 # new dict for changes
417 request = prepare_put_request(request,
421 request = prepare_put_request(request,
424 testcase.project_name)
425 request = prepare_put_request(request,
428 testcase.description)
430 # we raise an exception if there isn't a change
432 raise HTTPError(HTTP_FORBIDDEN,
435 # we merge the whole document """
436 edit_request = testcase.format()
437 edit_request.update(request)
439 """ Updating the DB """
440 yield self.db.testcases.update(query, edit_request)
441 new_case = yield self.db.testcases.find_one({"_id": testcase._id})
442 self.finish_request(format_data(new_case, Testcase))
446 def delete(self, project_name, case_name):
447 """ Remove a test case"""
449 query = {'project_name': project_name, 'name': case_name}
451 # check for an existing case to be deleted
452 testcase = yield self.db.testcases.find_one(query)
454 raise HTTPError(HTTP_NOT_FOUND,
455 "{}/{} could not be found as a case to be deleted"
456 .format(project_name, case_name))
458 # just delete it, or maybe save it elsewhere in a future
459 yield self.db.testcases.remove(query)
460 self.finish_request()
463 class TestResultsHandler(GenericApiHandler):
465 TestResultsHandler Class
466 Handle the requests about the Test project's results
468 - GET : Get all test results and details about a specific one
469 - POST : Add a test results
470 - DELETE : Remove a test result
473 def initialize(self):
474 """ Prepares the database for the entire class """
475 super(TestResultsHandler, self).initialize()
476 self.name = "test_result"
480 def get(self, result_id=None):
482 Retrieve result(s) for a test project on a specific POD.
483 Available filters for this request are :
484 - project : project name
487 - version : platform version (Arno-R1, ...)
488 - installer (fuel, ...)
489 - build_tag : Jenkins build tag name
490 - period : x (x last days)
491 - scenario : the test scenario (previously version)
492 - criteria : the global criteria status passed or failed
493 - trust_indicator : evaluate the stability of the test case to avoid
494 running systematically long and stable test case
497 :param result_id: Get a result by ID
500 GET /results/project=functest&case=vPing&version=Arno-R1 \
501 &pod=pod_name&period=15
502 => get results with optional filters
505 project_arg = self.get_query_argument("project", None)
506 case_arg = self.get_query_argument("case", None)
507 pod_arg = self.get_query_argument("pod", None)
508 version_arg = self.get_query_argument("version", None)
509 installer_arg = self.get_query_argument("installer", None)
510 build_tag_arg = self.get_query_argument("build_tag", None)
511 scenario_arg = self.get_query_argument("scenario", None)
512 criteria_arg = self.get_query_argument("criteria", None)
513 period_arg = self.get_query_argument("period", None)
514 trust_indicator_arg = self.get_query_argument("trust_indicator", None)
518 if result_id is None:
519 if project_arg is not None:
520 get_request["project_name"] = project_arg
522 if case_arg is not None:
523 get_request["case_name"] = case_arg
525 if pod_arg is not None:
526 get_request["pod_name"] = pod_arg
528 if version_arg is not None:
529 get_request["version"] = version_arg
531 if installer_arg is not None:
532 get_request["installer"] = installer_arg
534 if build_tag_arg is not None:
535 get_request["build_tag"] = build_tag_arg
537 if scenario_arg is not None:
538 get_request["scenario"] = scenario_arg
540 if criteria_arg is not None:
541 get_request["criteria_tag"] = criteria_arg
543 if trust_indicator_arg is not None:
544 get_request["trust_indicator_arg"] = trust_indicator_arg
546 if period_arg is not None:
548 period_arg = int(period_arg)
550 raise HTTPError(HTTP_BAD_REQUEST)
553 period = datetime.now() - timedelta(days=period_arg)
554 obj = {"$gte": str(period)}
555 get_request["creation_date"] = obj
557 get_request["_id"] = result_id
562 cursor = self.db.test_results.find(get_request)
563 while (yield cursor.fetch_next):
564 test_result = TestResult.test_result_from_dict(
565 cursor.next_object())
566 res.append(test_result.format_http())
568 # building meta object
570 meta["total"] = len(res)
572 # final response object
574 answer["test_results"] = res
575 answer["meta"] = meta
576 self.finish_request(answer)
582 Create a new test result
583 :return: status of the request
587 # check for request payload
588 if self.json_args is None:
589 raise HTTPError(HTTP_BAD_REQUEST)
591 # check for missing parameters in the request payload
592 if self.json_args.get("project_name") is None:
593 raise HTTPError(HTTP_BAD_REQUEST)
594 if self.json_args.get("case_name") is None:
595 raise HTTPError(HTTP_BAD_REQUEST)
596 # check for pod_name instead of id,
597 # keeping id for current implementations
598 if self.json_args.get("pod_name") is None:
599 raise HTTPError(HTTP_BAD_REQUEST)
601 # TODO : replace checks with jsonschema
603 mongo_dict = yield self.db.projects.find_one(
604 {"name": self.json_args.get("project_name")})
605 if mongo_dict is None:
606 raise HTTPError(HTTP_NOT_FOUND,
607 "Could not find project [{}] "
608 .format(self.json_args.get("project_name")))
611 mongo_dict = yield self.db.testcases.find_one(
612 {"name": self.json_args.get("case_name")})
613 if mongo_dict is None:
614 raise HTTPError(HTTP_NOT_FOUND,
615 "Could not find case [{}] "
616 .format(self.json_args.get("case_name")))
619 mongo_dict = yield self.db.pods.find_one(
620 {"name": self.json_args.get("pod_name")})
621 if mongo_dict is None:
622 raise HTTPError(HTTP_NOT_FOUND,
623 "Could not find POD [{}] "
624 .format(self.json_args.get("pod_name")))
626 # convert payload to object
627 test_result = TestResult.test_result_from_dict(self.json_args)
628 test_result.creation_date = datetime.now()
630 future = self.db.test_results.insert(test_result.format(),
632 result = yield future
633 test_result._id = result
635 self.finish_request(test_result.format_http())
638 class DashboardHandler(GenericApiHandler):
640 DashboardHandler Class
641 Handle the requests about the Test project's results
642 in a dahboard ready format
644 - GET : Get all test results and details about a specific one
646 def initialize(self):
647 """ Prepares the database for the entire class """
648 super(DashboardHandler, self).initialize()
649 self.name = "dashboard"
653 def get(self, result_id=None):
655 Retrieve dashboard ready result(s) for a test project
656 Available filters for this request are :
657 - project : project name
660 - version : platform version (Arno-R1, ...)
661 - installer (fuel, ...)
662 - period : x (x last days)
665 :param result_id: Get a result by ID
668 GET /dashboard?project=functest&case=vPing&version=Arno-R1 \
669 &pod=pod_name&period=15
670 => get results with optional filters
673 project_arg = self.get_query_argument("project", None)
674 case_arg = self.get_query_argument("case", None)
675 pod_arg = self.get_query_argument("pod", None)
676 version_arg = self.get_query_argument("version", None)
677 installer_arg = self.get_query_argument("installer", None)
678 period_arg = self.get_query_argument("period", None)
683 # /dashboard?project=<>&pod=<>...
684 if (result_id is None):
685 if project_arg is not None:
686 get_request["project_name"] = project_arg
688 if case_arg is not None:
689 get_request["case_name"] = case_arg
691 if pod_arg is not None:
692 get_request["pod_name"] = pod_arg
694 if version_arg is not None:
695 get_request["version"] = version_arg
697 if installer_arg is not None:
698 get_request["installer"] = installer_arg
700 if period_arg is not None:
702 period_arg = int(period_arg)
704 raise HTTPError(HTTP_BAD_REQUEST)
706 period = datetime.now() - timedelta(days=period_arg)
707 obj = {"$gte": str(period)}
708 get_request["creation_date"] = obj
710 get_request["_id"] = result_id
714 # on /dashboard retrieve the list of projects and testcases
715 # ready for dashboard
716 if project_arg is None:
717 raise HTTPError(HTTP_NOT_FOUND,
718 "error:Project name missing")
719 elif check_dashboard_ready_project(project_arg, "./dashboard"):
725 "error:Test case missing for project " + project_arg)
727 # special case of status for project
728 if case_arg == "status":
729 del get_request["case_name"]
730 # retention time to be agreed
731 # last five days by default?
733 period = datetime.now() - timedelta(days=5)
734 get_request["creation_date"] = {"$gte": period}
737 cursor = self.db.test_results.find(get_request)
738 while (yield cursor.fetch_next):
739 test_result = TestResult.test_result_from_dict(
740 cursor.next_object())
741 res.append(test_result.format_http())
743 if check_dashboard_ready_case(project_arg, case_arg):
744 dashboard = get_dashboard_result(project_arg, case_arg, res)
748 "error:" + case_arg +
749 " test case not case dashboard ready on project " +
754 {"error": "Project not recognized or not dashboard ready"})
756 {"Dashboard-ready-projects":
757 get_dashboard_cases("./dashboard")})
760 "error: no dashboard ready data for this project")
763 # cursor = self.db.test_results.find(get_request)
764 # while (yield cursor.fetch_next):
765 # test_result = TestResult.test_result_from_dict(
766 # cursor.next_object())
767 # res.append(test_result.format_http())
769 # building meta object
772 # final response object
774 answer["dashboard"] = dashboard
775 answer["meta"] = meta
776 self.finish_request(answer)