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 # feng.xiaowei@zte.com.cn add methods to GenericApiHandler 5-26-2016
16 # feng.xiaowei@zte.com.cn remove PodHandler 5-26-2016
17 ##############################################################################
20 from datetime import datetime, timedelta
22 from tornado.web import RequestHandler, asynchronous, HTTPError
23 from tornado import gen
25 from models import CreateResponse
26 from resources.result_models import TestResult
27 from resources.testcase_models import Testcase
28 from resources.project_models import Project
29 from common.constants import DEFAULT_REPRESENTATION, HTTP_BAD_REQUEST, \
30 HTTP_NOT_FOUND, HTTP_FORBIDDEN
31 from common.config import prepare_put_request
32 from dashboard.dashboard_utils import check_dashboard_ready_project, \
33 check_dashboard_ready_case, get_dashboard_result
36 def format_data(data, cls):
37 cls_data = cls.from_dict(data)
38 return cls_data.format_http()
41 class GenericApiHandler(RequestHandler):
43 The purpose of this class is to take benefit of inheritance and prepare
44 a set of common functions for
49 """ Prepares the database for the entire class """
50 self.db = self.settings["db"]
53 if self.request.method != "GET" and self.request.method != "DELETE":
54 if self.request.headers.get("Content-Type") is not None:
55 if self.request.headers["Content-Type"].startswith(
56 DEFAULT_REPRESENTATION):
58 self.json_args = json.loads(self.request.body)
59 except (ValueError, KeyError, TypeError) as error:
60 raise HTTPError(HTTP_BAD_REQUEST,
61 "Bad Json format [{}]".
66 def finish_request(self, json_object=None):
68 self.write(json.dumps(json_object))
69 self.set_header("Content-Type", DEFAULT_REPRESENTATION)
72 def _create_response(self, resource):
73 href = self.request.full_url() + '/' + resource
74 return CreateResponse(href=href).format()
78 def _create(self, table, data, mark):
79 data.creation_date = datetime.now()
80 _id = yield self._eval_db(table, 'insert', data.format())
83 self.finish_request(self._create_response(mark))
87 def _list(self, table, format_cls, query=None):
91 cursor = self._eval_db(table, 'find', query)
92 while (yield cursor.fetch_next):
93 res.append(format_data(cursor.next_object(), format_cls))
94 self.finish_request({table: res})
98 def _get_one(self, table, format_cls, query):
99 data = yield self._eval_db(table, 'find_one', query)
101 raise HTTPError(HTTP_NOT_FOUND,
102 "{} Not Exist".format(query))
103 self.finish_request(format_data(data, format_cls))
105 def _eval_db(self, table, method, param):
106 return eval('self.db.%s.%s(param)' % (table, method))
109 class VersionHandler(GenericApiHandler):
110 """ Display a message for the API version """
112 self.finish_request([{'v1': 'basics'}])
115 class ProjectHandler(GenericApiHandler):
117 TestProjectHandler Class
118 Handle the requests about the Test projects
120 - GET : Get all test projects and details about a specific one
121 - POST : Add a test project
122 - PUT : Edit test projects information (name and/or description)
123 - DELETE : Remove a test project
126 def initialize(self):
127 """ Prepares the database for the entire class """
128 super(ProjectHandler, self).initialize()
132 def get(self, project_name=None):
140 if project_name is not None:
141 query["name"] = project_name
142 answer = yield self.db.projects.find_one(query)
144 raise HTTPError(HTTP_NOT_FOUND,
145 "{} Not Exist".format(project_name))
147 answer = format_data(answer, Project)
150 cursor = self.db.projects.find(query)
151 while (yield cursor.fetch_next):
152 res.append(format_data(cursor.next_object(), Project))
153 answer = {'projects': res}
155 self.finish_request(answer)
160 """ Create a test project"""
162 if self.json_args is None:
163 raise HTTPError(HTTP_BAD_REQUEST)
165 query = {"name": self.json_args.get("name")}
167 # check for name in db
168 the_project = yield self.db.projects.find_one(query)
169 if the_project is not None:
170 raise HTTPError(HTTP_FORBIDDEN,
171 "{} already exists as a project".format(
172 self.json_args.get("name")))
174 project = Project.from_dict(self.json_args)
175 project.creation_date = datetime.now()
177 yield self.db.projects.insert(project.format())
178 self.finish_request(self._create_response(project.name))
182 def put(self, project_name):
183 """ Updates the name and description of a test project"""
185 if self.json_args is None:
186 raise HTTPError(HTTP_BAD_REQUEST)
188 query = {'name': project_name}
189 from_project = yield self.db.projects.find_one(query)
190 if from_project is None:
191 raise HTTPError(HTTP_NOT_FOUND,
192 "{} could not be found".format(project_name))
194 project = Project.from_dict(from_project)
195 new_name = self.json_args.get("name")
196 new_description = self.json_args.get("description")
198 # check for payload name parameter in db
199 # avoid a request if the project name has not changed in the payload
200 if new_name != project.name:
201 to_project = yield self.db.projects.find_one(
203 if to_project is not None:
204 raise HTTPError(HTTP_FORBIDDEN,
205 "{} already exists as a project"
208 # new dict for changes
210 request = prepare_put_request(request,
214 request = prepare_put_request(request,
219 """ raise exception if there isn't a change """
221 raise HTTPError(HTTP_FORBIDDEN, "Nothing to update")
223 """ we merge the whole document """
224 edit_request = project.format()
225 edit_request.update(request)
227 """ Updating the DB """
228 yield self.db.projects.update({'name': project_name}, edit_request)
229 new_project = yield self.db.projects.find_one({"_id": project._id})
231 self.finish_request(format_data(new_project, Project))
235 def delete(self, project_name):
236 """ Remove a test project"""
237 query = {'name': project_name}
239 # check for an existing project to be deleted
240 project = yield self.db.projects.find_one(query)
242 raise HTTPError(HTTP_NOT_FOUND,
243 "{} could not be found as a project to be deleted"
244 .format(project_name))
246 # just delete it, or maybe save it elsewhere in a future
247 yield self.db.projects.remove(query)
249 self.finish_request()
252 class TestcaseHandler(GenericApiHandler):
254 TestCasesHandler Class
255 Handle the requests about the Test cases for test projects
257 - GET : Get all test cases and details about a specific one
258 - POST : Add a test project
259 - PUT : Edit test projects information (name and/or description)
262 def initialize(self):
263 """ Prepares the database for the entire class """
264 super(TestcaseHandler, self).initialize()
268 def get(self, project_name, case_name=None):
270 Get testcases(s) info
275 query = {'project_name': project_name}
277 if case_name is not None:
278 query["name"] = case_name
279 answer = yield self.db.testcases.find_one(query)
281 raise HTTPError(HTTP_NOT_FOUND,
282 "{} Not Exist".format(case_name))
284 answer = format_data(answer, Testcase)
287 cursor = self.db.testcases.find(query)
288 while (yield cursor.fetch_next):
289 res.append(format_data(cursor.next_object(), Testcase))
290 answer = {'testcases': res}
292 self.finish_request(answer)
296 def post(self, project_name):
297 """ Create a test case"""
299 if self.json_args is None:
300 raise HTTPError(HTTP_BAD_REQUEST,
301 "Check your request payload")
303 # retrieve test project
304 project = yield self.db.projects.find_one(
305 {"name": project_name})
307 raise HTTPError(HTTP_FORBIDDEN,
308 "Could not find project {}"
309 .format(project_name))
311 case_name = self.json_args.get('name')
312 the_testcase = yield self.db.testcases.find_one(
313 {"project_name": project_name, 'name': case_name})
315 raise HTTPError(HTTP_FORBIDDEN,
316 "{} already exists as a case in project {}"
317 .format(case_name, project_name))
319 testcase = Testcase.from_dict(self.json_args)
320 testcase.project_name = project_name
321 testcase.creation_date = datetime.now()
323 yield self.db.testcases.insert(testcase.format())
324 self.finish_request(self._create_response(testcase.name))
328 def put(self, project_name, case_name):
330 Updates the name and description of a test case
331 :raises HTTPError (HTTP_NOT_FOUND, HTTP_FORBIDDEN)
334 query = {'project_name': project_name, 'name': case_name}
336 if self.json_args is None:
337 raise HTTPError(HTTP_BAD_REQUEST, "No payload")
339 # check if there is a case for the project in url parameters
340 from_testcase = yield self.db.testcases.find_one(query)
341 if from_testcase is None:
342 raise HTTPError(HTTP_NOT_FOUND,
343 "{} could not be found as a {} case to be updated"
344 .format(case_name, project_name))
346 testcase = Testcase.from_dict(from_testcase)
347 new_name = self.json_args.get("name")
348 new_project_name = self.json_args.get("project_name")
349 if not new_project_name:
350 new_project_name = project_name
351 new_description = self.json_args.get("description")
353 # check if there is not an existing test case
354 # with the name provided in the json payload
355 if new_name != case_name or new_project_name != project_name:
356 to_testcase = yield self.db.testcases.find_one(
357 {'project_name': new_project_name, 'name': new_name})
358 if to_testcase is not None:
359 raise HTTPError(HTTP_FORBIDDEN,
360 "{} already exists as a case in project"
361 .format(new_name, new_project_name))
363 # new dict for changes
365 request = prepare_put_request(request,
369 request = prepare_put_request(request,
372 testcase.project_name)
373 request = prepare_put_request(request,
376 testcase.description)
378 # we raise an exception if there isn't a change
380 raise HTTPError(HTTP_FORBIDDEN,
383 # we merge the whole document """
384 edit_request = testcase.format()
385 edit_request.update(request)
387 """ Updating the DB """
388 yield self.db.testcases.update(query, edit_request)
389 new_case = yield self.db.testcases.find_one({"_id": testcase._id})
390 self.finish_request(format_data(new_case, Testcase))
394 def delete(self, project_name, case_name):
395 """ Remove a test case"""
397 query = {'project_name': project_name, 'name': case_name}
399 # check for an existing case to be deleted
400 testcase = yield self.db.testcases.find_one(query)
402 raise HTTPError(HTTP_NOT_FOUND,
403 "{}/{} could not be found as a case to be deleted"
404 .format(project_name, case_name))
406 # just delete it, or maybe save it elsewhere in a future
407 yield self.db.testcases.remove(query)
408 self.finish_request()
411 class TestResultsHandler(GenericApiHandler):
413 TestResultsHandler Class
414 Handle the requests about the Test project's results
416 - GET : Get all test results and details about a specific one
417 - POST : Add a test results
418 - DELETE : Remove a test result
421 def initialize(self):
422 """ Prepares the database for the entire class """
423 super(TestResultsHandler, self).initialize()
424 self.name = "test_result"
428 def get(self, result_id=None):
430 Retrieve result(s) for a test project on a specific POD.
431 Available filters for this request are :
432 - project : project name
435 - version : platform version (Arno-R1, ...)
436 - installer (fuel, ...)
437 - build_tag : Jenkins build tag name
438 - period : x (x last days)
439 - scenario : the test scenario (previously version)
440 - criteria : the global criteria status passed or failed
441 - trust_indicator : evaluate the stability of the test case to avoid
442 running systematically long and stable test case
445 :param result_id: Get a result by ID
448 GET /results/project=functest&case=vPing&version=Arno-R1 \
449 &pod=pod_name&period=15
450 => get results with optional filters
455 if result_id is not None:
456 query["_id"] = result_id
457 answer = yield self.db.results.find_one(query)
459 raise HTTPError(HTTP_NOT_FOUND,
460 "test result {} Not Exist".format(result_id))
462 answer = format_data(answer, TestResult)
464 pod_arg = self.get_query_argument("pod", None)
465 project_arg = self.get_query_argument("project", None)
466 case_arg = self.get_query_argument("case", None)
467 version_arg = self.get_query_argument("version", None)
468 installer_arg = self.get_query_argument("installer", None)
469 build_tag_arg = self.get_query_argument("build_tag", None)
470 scenario_arg = self.get_query_argument("scenario", None)
471 criteria_arg = self.get_query_argument("criteria", None)
472 period_arg = self.get_query_argument("period", None)
473 trust_indicator_arg = self.get_query_argument("trust_indicator",
476 if project_arg is not None:
477 query["project_name"] = project_arg
479 if case_arg is not None:
480 query["case_name"] = case_arg
482 if pod_arg is not None:
483 query["pod_name"] = pod_arg
485 if version_arg is not None:
486 query["version"] = version_arg
488 if installer_arg is not None:
489 query["installer"] = installer_arg
491 if build_tag_arg is not None:
492 query["build_tag"] = build_tag_arg
494 if scenario_arg is not None:
495 query["scenario"] = scenario_arg
497 if criteria_arg is not None:
498 query["criteria_tag"] = criteria_arg
500 if trust_indicator_arg is not None:
501 query["trust_indicator_arg"] = trust_indicator_arg
503 if period_arg is not None:
505 period_arg = int(period_arg)
507 raise HTTPError(HTTP_BAD_REQUEST)
510 period = datetime.now() - timedelta(days=period_arg)
511 obj = {"$gte": str(period)}
512 query["creation_date"] = obj
515 cursor = self.db.results.find(query)
516 while (yield cursor.fetch_next):
517 res.append(format_data(cursor.next_object(), TestResult))
518 answer = {'results': res}
520 self.finish_request(answer)
526 Create a new test result
527 :return: status of the request
531 # check for request payload
532 if self.json_args is None:
533 raise HTTPError(HTTP_BAD_REQUEST, 'no payload')
535 result = TestResult.from_dict(self.json_args)
537 # check for pod_name instead of id,
538 # keeping id for current implementations
539 if result.pod_name is None:
540 raise HTTPError(HTTP_BAD_REQUEST, 'pod is not provided')
542 # check for missing parameters in the request payload
543 if result.project_name is None:
544 raise HTTPError(HTTP_BAD_REQUEST, 'project is not provided')
546 if result.case_name is None:
547 raise HTTPError(HTTP_BAD_REQUEST, 'testcase is not provided')
549 # TODO : replace checks with jsonschema
551 the_pod = yield self.db.pods.find_one({"name": result.pod_name})
553 raise HTTPError(HTTP_NOT_FOUND,
554 "Could not find POD [{}] "
555 .format(self.json_args.get("pod_name")))
558 the_project = yield self.db.projects.find_one(
559 {"name": result.project_name})
560 if the_project is None:
561 raise HTTPError(HTTP_NOT_FOUND, "Could not find project [{}] "
562 .format(result.project_name))
565 the_testcase = yield self.db.testcases.find_one(
566 {"name": result.case_name})
567 if the_testcase is None:
568 raise HTTPError(HTTP_NOT_FOUND,
569 "Could not find testcase [{}] "
570 .format(result.case_name))
572 _id = yield self.db.results.insert(result.format(), check_keys=False)
574 self.finish_request(self._create_response(_id))
577 class DashboardHandler(GenericApiHandler):
579 DashboardHandler Class
580 Handle the requests about the Test project's results
581 in a dahboard ready format
583 - GET : Get all test results and details about a specific one
585 def initialize(self):
586 """ Prepares the database for the entire class """
587 super(DashboardHandler, self).initialize()
588 self.name = "dashboard"
594 Retrieve dashboard ready result(s) for a test project
595 Available filters for this request are :
596 - project : project name
599 - version : platform version (Arno-R1, ...)
600 - installer (fuel, ...)
601 - period : x (x last days)
604 :param result_id: Get a result by ID
607 GET /dashboard?project=functest&case=vPing&version=Arno-R1 \
608 &pod=pod_name&period=15
609 => get results with optional filters
612 project_arg = self.get_query_argument("project", None)
613 case_arg = self.get_query_argument("case", None)
614 pod_arg = self.get_query_argument("pod", None)
615 version_arg = self.get_query_argument("version", None)
616 installer_arg = self.get_query_argument("installer", None)
617 period_arg = self.get_query_argument("period", None)
622 if project_arg is not None:
623 query["project_name"] = project_arg
625 if case_arg is not None:
626 query["case_name"] = case_arg
628 if pod_arg is not None:
629 query["pod_name"] = pod_arg
631 if version_arg is not None:
632 query["version"] = version_arg
634 if installer_arg is not None:
635 query["installer"] = installer_arg
637 if period_arg is not None:
639 period_arg = int(period_arg)
641 raise HTTPError(HTTP_BAD_REQUEST)
643 period = datetime.now() - timedelta(days=period_arg)
644 obj = {"$gte": str(period)}
645 query["creation_date"] = obj
647 # on /dashboard retrieve the list of projects and testcases
648 # ready for dashboard
649 if project_arg is None:
650 raise HTTPError(HTTP_NOT_FOUND, "Project name missing")
652 if not check_dashboard_ready_project(project_arg):
653 raise HTTPError(HTTP_NOT_FOUND,
654 'Project [{}] not dashboard ready'
655 .format(project_arg))
660 'Test case missing for project [{}]'.format(project_arg))
662 if not check_dashboard_ready_case(project_arg, case_arg):
665 'Test case [{}] not dashboard ready for project [{}]'
666 .format(case_arg, project_arg))
668 # special case of status for project
670 if case_arg != "status":
671 cursor = self.db.results.find(query)
672 while (yield cursor.fetch_next):
673 result = TestResult.from_dict(cursor.next_object())
674 res.append(result.format_http())
676 # final response object
677 self.finish_request(get_dashboard_result(project_arg, case_arg, res))