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 testcase related handler 5-20-2016
13 # feng.xiaowei@zte.com.cn refactor result related handler 5-23-2016
14 # feng.xiaowei@zte.com.cn refactor dashboard related handler 5-24-2016
15 ##############################################################################
18 from datetime import datetime, timedelta
20 from tornado.web import RequestHandler, asynchronous, HTTPError
21 from tornado import gen
23 from models import CreateResponse
24 from resources.result_models import TestResult
25 from resources.testcase_models import Testcase
26 from resources.project_models import Project
27 from resources.pod_models import Pod
28 from common.constants import DEFAULT_REPRESENTATION, HTTP_BAD_REQUEST, \
29 HTTP_NOT_FOUND, HTTP_FORBIDDEN
30 from common.config import prepare_put_request
31 from dashboard.dashboard_utils import check_dashboard_ready_project, \
32 check_dashboard_ready_case, get_dashboard_result
35 def format_data(data, cls):
36 cls_data = cls.from_dict(data)
37 return cls_data.format_http()
40 class GenericApiHandler(RequestHandler):
42 The purpose of this class is to take benefit of inheritance and prepare
43 a set of common functions for
48 """ Prepares the database for the entire class """
49 self.db = self.settings["db"]
52 if self.request.method != "GET" and self.request.method != "DELETE":
53 if self.request.headers.get("Content-Type") is not None:
54 if self.request.headers["Content-Type"].startswith(
55 DEFAULT_REPRESENTATION):
57 self.json_args = json.loads(self.request.body)
58 except (ValueError, KeyError, TypeError) as error:
59 raise HTTPError(HTTP_BAD_REQUEST,
60 "Bad Json format [{}]".
65 def finish_request(self, json_object=None):
67 self.write(json.dumps(json_object))
68 self.set_header("Content-Type", DEFAULT_REPRESENTATION)
71 def _create_response(self, resource):
72 href = self.request.full_url() + '/' + resource
73 return CreateResponse(href=href).format()
76 class VersionHandler(RequestHandler):
77 """ Display a message for the API version """
79 self.write("Collection of test result API, v1")
82 class PodHandler(GenericApiHandler):
83 """ Handle the requests about the POD Platforms
91 """ Prepares the database for the entire class """
92 super(PodHandler, self).initialize()
96 def get(self, pod_name=None):
98 Get all pods or a single pod
103 if pod_name is not None:
104 query["name"] = pod_name
105 answer = yield self.db.pods.find_one(query)
107 raise HTTPError(HTTP_NOT_FOUND,
108 "{} Not Exist".format(pod_name))
110 answer = format_data(answer, Pod)
113 cursor = self.db.pods.find(query)
114 while (yield cursor.fetch_next):
115 res.append(format_data(cursor.next_object(), Pod))
116 answer = {'pods': res}
118 self.finish_request(answer)
125 if self.json_args is None:
126 raise HTTPError(HTTP_BAD_REQUEST)
128 query = {"name": self.json_args.get("name")}
130 # check for existing name in db
131 the_pod = yield self.db.pods.find_one(query)
132 if the_pod is not None:
133 raise HTTPError(HTTP_FORBIDDEN,
134 "{} already exists as a pod".format(
135 self.json_args.get("name")))
137 pod = Pod.from_dict(self.json_args)
138 pod.creation_date = datetime.now()
140 yield self.db.pods.insert(pod.format())
141 self.finish_request(self._create_response(pod.name))
145 def delete(self, pod_name):
148 # check for an existing pod to be deleted
149 mongo_dict = yield self.db.pods.find_one(
151 pod = TestProject.pod(mongo_dict)
153 raise HTTPError(HTTP_NOT_FOUND,
154 "{} could not be found as a pod to be deleted"
157 # just delete it, or maybe save it elsewhere in a future
158 res = yield self.db.projects.remove(
161 self.finish_request(answer)
166 class ProjectHandler(GenericApiHandler):
168 TestProjectHandler Class
169 Handle the requests about the Test projects
171 - GET : Get all test projects and details about a specific one
172 - POST : Add a test project
173 - PUT : Edit test projects information (name and/or description)
174 - DELETE : Remove a test project
177 def initialize(self):
178 """ Prepares the database for the entire class """
179 super(ProjectHandler, self).initialize()
183 def get(self, project_name=None):
191 if project_name is not None:
192 query["name"] = project_name
193 answer = yield self.db.projects.find_one(query)
195 raise HTTPError(HTTP_NOT_FOUND,
196 "{} Not Exist".format(project_name))
198 answer = format_data(answer, Project)
201 cursor = self.db.projects.find(query)
202 while (yield cursor.fetch_next):
203 res.append(format_data(cursor.next_object(), Project))
204 answer = {'projects': res}
206 self.finish_request(answer)
211 """ Create a test project"""
213 if self.json_args is None:
214 raise HTTPError(HTTP_BAD_REQUEST)
216 query = {"name": self.json_args.get("name")}
218 # check for name in db
219 the_project = yield self.db.projects.find_one(query)
220 if the_project is not None:
221 raise HTTPError(HTTP_FORBIDDEN,
222 "{} already exists as a project".format(
223 self.json_args.get("name")))
225 project = Project.from_dict(self.json_args)
226 project.creation_date = datetime.now()
228 yield self.db.projects.insert(project.format())
229 self.finish_request(self._create_response(project.name))
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 self.finish_request(self._create_response(testcase.name))
379 def put(self, project_name, case_name):
381 Updates the name and description of a test case
382 :raises HTTPError (HTTP_NOT_FOUND, HTTP_FORBIDDEN)
385 query = {'project_name': project_name, 'name': case_name}
387 if self.json_args is None:
388 raise HTTPError(HTTP_BAD_REQUEST, "No payload")
390 # check if there is a case for the project in url parameters
391 from_testcase = yield self.db.testcases.find_one(query)
392 if from_testcase is None:
393 raise HTTPError(HTTP_NOT_FOUND,
394 "{} could not be found as a {} case to be updated"
395 .format(case_name, project_name))
397 testcase = Testcase.from_dict(from_testcase)
398 new_name = self.json_args.get("name")
399 new_project_name = self.json_args.get("project_name")
400 if not new_project_name:
401 new_project_name = project_name
402 new_description = self.json_args.get("description")
404 # check if there is not an existing test case
405 # with the name provided in the json payload
406 if new_name != case_name or new_project_name != project_name:
407 to_testcase = yield self.db.testcases.find_one(
408 {'project_name': new_project_name, 'name': new_name})
409 if to_testcase is not None:
410 raise HTTPError(HTTP_FORBIDDEN,
411 "{} already exists as a case in project"
412 .format(new_name, new_project_name))
414 # new dict for changes
416 request = prepare_put_request(request,
420 request = prepare_put_request(request,
423 testcase.project_name)
424 request = prepare_put_request(request,
427 testcase.description)
429 # we raise an exception if there isn't a change
431 raise HTTPError(HTTP_FORBIDDEN,
434 # we merge the whole document """
435 edit_request = testcase.format()
436 edit_request.update(request)
438 """ Updating the DB """
439 yield self.db.testcases.update(query, edit_request)
440 new_case = yield self.db.testcases.find_one({"_id": testcase._id})
441 self.finish_request(format_data(new_case, Testcase))
445 def delete(self, project_name, case_name):
446 """ Remove a test case"""
448 query = {'project_name': project_name, 'name': case_name}
450 # check for an existing case to be deleted
451 testcase = yield self.db.testcases.find_one(query)
453 raise HTTPError(HTTP_NOT_FOUND,
454 "{}/{} could not be found as a case to be deleted"
455 .format(project_name, case_name))
457 # just delete it, or maybe save it elsewhere in a future
458 yield self.db.testcases.remove(query)
459 self.finish_request()
462 class TestResultsHandler(GenericApiHandler):
464 TestResultsHandler Class
465 Handle the requests about the Test project's results
467 - GET : Get all test results and details about a specific one
468 - POST : Add a test results
469 - DELETE : Remove a test result
472 def initialize(self):
473 """ Prepares the database for the entire class """
474 super(TestResultsHandler, self).initialize()
475 self.name = "test_result"
479 def get(self, result_id=None):
481 Retrieve result(s) for a test project on a specific POD.
482 Available filters for this request are :
483 - project : project name
486 - version : platform version (Arno-R1, ...)
487 - installer (fuel, ...)
488 - build_tag : Jenkins build tag name
489 - period : x (x last days)
490 - scenario : the test scenario (previously version)
491 - criteria : the global criteria status passed or failed
492 - trust_indicator : evaluate the stability of the test case to avoid
493 running systematically long and stable test case
496 :param result_id: Get a result by ID
499 GET /results/project=functest&case=vPing&version=Arno-R1 \
500 &pod=pod_name&period=15
501 => get results with optional filters
506 if result_id is not None:
507 query["_id"] = result_id
508 answer = yield self.db.results.find_one(query)
510 raise HTTPError(HTTP_NOT_FOUND,
511 "test result {} Not Exist".format(result_id))
513 answer = format_data(answer, TestResult)
515 pod_arg = self.get_query_argument("pod", None)
516 project_arg = self.get_query_argument("project", None)
517 case_arg = self.get_query_argument("case", None)
518 version_arg = self.get_query_argument("version", None)
519 installer_arg = self.get_query_argument("installer", None)
520 build_tag_arg = self.get_query_argument("build_tag", None)
521 scenario_arg = self.get_query_argument("scenario", None)
522 criteria_arg = self.get_query_argument("criteria", None)
523 period_arg = self.get_query_argument("period", None)
524 trust_indicator_arg = self.get_query_argument("trust_indicator",
527 if project_arg is not None:
528 query["project_name"] = project_arg
530 if case_arg is not None:
531 query["case_name"] = case_arg
533 if pod_arg is not None:
534 query["pod_name"] = pod_arg
536 if version_arg is not None:
537 query["version"] = version_arg
539 if installer_arg is not None:
540 query["installer"] = installer_arg
542 if build_tag_arg is not None:
543 query["build_tag"] = build_tag_arg
545 if scenario_arg is not None:
546 query["scenario"] = scenario_arg
548 if criteria_arg is not None:
549 query["criteria_tag"] = criteria_arg
551 if trust_indicator_arg is not None:
552 query["trust_indicator_arg"] = trust_indicator_arg
554 if period_arg is not None:
556 period_arg = int(period_arg)
558 raise HTTPError(HTTP_BAD_REQUEST)
561 period = datetime.now() - timedelta(days=period_arg)
562 obj = {"$gte": str(period)}
563 query["creation_date"] = obj
566 cursor = self.db.results.find(query)
567 while (yield cursor.fetch_next):
568 res.append(format_data(cursor.next_object(), TestResult))
569 answer = {'results': res}
571 self.finish_request(answer)
577 Create a new test result
578 :return: status of the request
582 # check for request payload
583 if self.json_args is None:
584 raise HTTPError(HTTP_BAD_REQUEST, 'no payload')
586 result = TestResult.from_dict(self.json_args)
588 # check for pod_name instead of id,
589 # keeping id for current implementations
590 if result.pod_name is None:
591 raise HTTPError(HTTP_BAD_REQUEST, 'pod is not provided')
593 # check for missing parameters in the request payload
594 if result.project_name is None:
595 raise HTTPError(HTTP_BAD_REQUEST, 'project is not provided')
597 if result.case_name is None:
598 raise HTTPError(HTTP_BAD_REQUEST, 'testcase is not provided')
600 # TODO : replace checks with jsonschema
602 the_pod = yield self.db.pods.find_one({"name": result.pod_name})
604 raise HTTPError(HTTP_NOT_FOUND,
605 "Could not find POD [{}] "
606 .format(self.json_args.get("pod_name")))
609 the_project = yield self.db.projects.find_one(
610 {"name": result.project_name})
611 if the_project is None:
612 raise HTTPError(HTTP_NOT_FOUND, "Could not find project [{}] "
613 .format(result.project_name))
616 the_testcase = yield self.db.testcases.find_one(
617 {"name": result.case_name})
618 if the_testcase is None:
619 raise HTTPError(HTTP_NOT_FOUND,
620 "Could not find testcase [{}] "
621 .format(result.case_name))
623 _id = yield self.db.results.insert(result.format(), check_keys=False)
625 self.finish_request(self._create_response(_id))
628 class DashboardHandler(GenericApiHandler):
630 DashboardHandler Class
631 Handle the requests about the Test project's results
632 in a dahboard ready format
634 - GET : Get all test results and details about a specific one
636 def initialize(self):
637 """ Prepares the database for the entire class """
638 super(DashboardHandler, self).initialize()
639 self.name = "dashboard"
645 Retrieve dashboard ready result(s) for a test project
646 Available filters for this request are :
647 - project : project name
650 - version : platform version (Arno-R1, ...)
651 - installer (fuel, ...)
652 - period : x (x last days)
655 :param result_id: Get a result by ID
658 GET /dashboard?project=functest&case=vPing&version=Arno-R1 \
659 &pod=pod_name&period=15
660 => get results with optional filters
663 project_arg = self.get_query_argument("project", None)
664 case_arg = self.get_query_argument("case", None)
665 pod_arg = self.get_query_argument("pod", None)
666 version_arg = self.get_query_argument("version", None)
667 installer_arg = self.get_query_argument("installer", None)
668 period_arg = self.get_query_argument("period", None)
673 if project_arg is not None:
674 query["project_name"] = project_arg
676 if case_arg is not None:
677 query["case_name"] = case_arg
679 if pod_arg is not None:
680 query["pod_name"] = pod_arg
682 if version_arg is not None:
683 query["version"] = version_arg
685 if installer_arg is not None:
686 query["installer"] = installer_arg
688 if period_arg is not None:
690 period_arg = int(period_arg)
692 raise HTTPError(HTTP_BAD_REQUEST)
694 period = datetime.now() - timedelta(days=period_arg)
695 obj = {"$gte": str(period)}
696 query["creation_date"] = obj
698 # on /dashboard retrieve the list of projects and testcases
699 # ready for dashboard
700 if project_arg is None:
701 raise HTTPError(HTTP_NOT_FOUND, "Project name missing")
703 if not check_dashboard_ready_project(project_arg):
704 raise HTTPError(HTTP_NOT_FOUND,
705 'Project [{}] not dashboard ready'
706 .format(project_arg))
711 'Test case missing for project [{}]'.format(project_arg))
713 if not check_dashboard_ready_case(project_arg, case_arg):
716 'Test case [{}] not dashboard ready for project [{}]'
717 .format(case_arg, project_arg))
719 # special case of status for project
721 if case_arg != "status":
722 cursor = self.db.results.find(query)
723 while (yield cursor.fetch_next):
724 result = TestResult.from_dict(cursor.next_object())
725 res.append(result.format_http())
727 # final response object
728 self.finish_request(get_dashboard_result(project_arg, case_arg, res))