afee1cd4643cda23cf155df03a88ab404cca6705
[releng.git] / utils / test / result_collection_api / resources / handlers.py
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 ##############################################################################
18
19 import json
20 from datetime import datetime, timedelta
21
22 from tornado.web import RequestHandler, asynchronous, HTTPError
23 from tornado import gen
24
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
34
35
36 def format_data(data, cls):
37     cls_data = cls.from_dict(data)
38     return cls_data.format_http()
39
40
41 class GenericApiHandler(RequestHandler):
42     """
43     The purpose of this class is to take benefit of inheritance and prepare
44     a set of common functions for
45     the handlers
46     """
47
48     def initialize(self):
49         """ Prepares the database for the entire class """
50         self.db = self.settings["db"]
51
52     def prepare(self):
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):
57                     try:
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 [{}]".
62                                         format(error))
63                 else:
64                     self.json_args = None
65
66     def finish_request(self, json_object=None):
67         if json_object:
68             self.write(json.dumps(json_object))
69         self.set_header("Content-Type", DEFAULT_REPRESENTATION)
70         self.finish()
71
72     def _create_response(self, resource):
73         href = self.request.full_url() + '/' + resource
74         return CreateResponse(href=href).format()
75
76     @asynchronous
77     @gen.coroutine
78     def _create(self, table, data, mark):
79         data.creation_date = datetime.now()
80         _id = yield self._eval_db(table, 'insert', data.format())
81         if mark is None:
82             mark = _id
83         self.finish_request(self._create_response(mark))
84
85     @asynchronous
86     @gen.coroutine
87     def _list(self, table, format_cls, query=None):
88         if query is None:
89             query = {}
90         res = []
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})
95
96     @asynchronous
97     @gen.coroutine
98     def _get_one(self, table, format_cls, query):
99         data = yield self._eval_db(table, 'find_one', query)
100         if data is None:
101             raise HTTPError(HTTP_NOT_FOUND,
102                             "{} Not Exist".format(query))
103         self.finish_request(format_data(data, format_cls))
104
105     def _eval_db(self, table, method, param):
106         return eval('self.db.%s.%s(param)' % (table, method))
107
108
109 class VersionHandler(GenericApiHandler):
110     """ Display a message for the API version """
111     def get(self):
112         self.finish_request([{'v1': 'basics'}])
113
114
115 class ProjectHandler(GenericApiHandler):
116     """
117     TestProjectHandler Class
118     Handle the requests about the Test projects
119     HTTP Methdods :
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
124     """
125
126     def initialize(self):
127         """ Prepares the database for the entire class """
128         super(ProjectHandler, self).initialize()
129
130     @asynchronous
131     @gen.coroutine
132     def get(self, project_name=None):
133         """
134         Get Project(s) info
135         :param project_name:
136         """
137
138         query = dict()
139
140         if project_name is not None:
141             query["name"] = project_name
142             answer = yield self.db.projects.find_one(query)
143             if answer is None:
144                 raise HTTPError(HTTP_NOT_FOUND,
145                                 "{} Not Exist".format(project_name))
146             else:
147                 answer = format_data(answer, Project)
148         else:
149             res = []
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}
154
155         self.finish_request(answer)
156
157     @asynchronous
158     @gen.coroutine
159     def post(self):
160         """ Create a test project"""
161
162         if self.json_args is None:
163             raise HTTPError(HTTP_BAD_REQUEST)
164
165         query = {"name": self.json_args.get("name")}
166
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")))
173
174         project = Project.from_dict(self.json_args)
175         project.creation_date = datetime.now()
176
177         yield self.db.projects.insert(project.format())
178         self.finish_request(self._create_response(project.name))
179
180     @asynchronous
181     @gen.coroutine
182     def put(self, project_name):
183         """ Updates the name and description of a test project"""
184
185         if self.json_args is None:
186             raise HTTPError(HTTP_BAD_REQUEST)
187
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))
193
194         project = Project.from_dict(from_project)
195         new_name = self.json_args.get("name")
196         new_description = self.json_args.get("description")
197
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(
202                 {"name": new_name})
203             if to_project is not None:
204                 raise HTTPError(HTTP_FORBIDDEN,
205                                 "{} already exists as a project"
206                                 .format(new_name))
207
208         # new dict for changes
209         request = dict()
210         request = prepare_put_request(request,
211                                       "name",
212                                       new_name,
213                                       project.name)
214         request = prepare_put_request(request,
215                                       "description",
216                                       new_description,
217                                       project.description)
218
219         """ raise exception if there isn't a change """
220         if not request:
221             raise HTTPError(HTTP_FORBIDDEN, "Nothing to update")
222
223         """ we merge the whole document """
224         edit_request = project.format()
225         edit_request.update(request)
226
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})
230
231         self.finish_request(format_data(new_project, Project))
232
233     @asynchronous
234     @gen.coroutine
235     def delete(self, project_name):
236         """ Remove a test project"""
237         query = {'name': project_name}
238
239         # check for an existing project to be deleted
240         project = yield self.db.projects.find_one(query)
241         if project is None:
242             raise HTTPError(HTTP_NOT_FOUND,
243                             "{} could not be found as a project to be deleted"
244                             .format(project_name))
245
246         # just delete it, or maybe save it elsewhere in a future
247         yield self.db.projects.remove(query)
248
249         self.finish_request()
250
251
252 class TestcaseHandler(GenericApiHandler):
253     """
254     TestCasesHandler Class
255     Handle the requests about the Test cases for test projects
256     HTTP Methdods :
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)
260     """
261
262     def initialize(self):
263         """ Prepares the database for the entire class """
264         super(TestcaseHandler, self).initialize()
265
266     @asynchronous
267     @gen.coroutine
268     def get(self, project_name, case_name=None):
269         """
270         Get testcases(s) info
271         :param project_name:
272         :param case_name:
273         """
274
275         query = {'project_name': project_name}
276
277         if case_name is not None:
278             query["name"] = case_name
279             answer = yield self.db.testcases.find_one(query)
280             if answer is None:
281                 raise HTTPError(HTTP_NOT_FOUND,
282                                 "{} Not Exist".format(case_name))
283             else:
284                 answer = format_data(answer, Testcase)
285         else:
286             res = []
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}
291
292         self.finish_request(answer)
293
294     @asynchronous
295     @gen.coroutine
296     def post(self, project_name):
297         """ Create a test case"""
298
299         if self.json_args is None:
300             raise HTTPError(HTTP_BAD_REQUEST,
301                             "Check your request payload")
302
303         # retrieve test project
304         project = yield self.db.projects.find_one(
305             {"name": project_name})
306         if project is None:
307             raise HTTPError(HTTP_FORBIDDEN,
308                             "Could not find project {}"
309                             .format(project_name))
310
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})
314         if the_testcase:
315             raise HTTPError(HTTP_FORBIDDEN,
316                             "{} already exists as a case in project {}"
317                             .format(case_name, project_name))
318
319         testcase = Testcase.from_dict(self.json_args)
320         testcase.project_name = project_name
321         testcase.creation_date = datetime.now()
322
323         yield self.db.testcases.insert(testcase.format())
324         self.finish_request(self._create_response(testcase.name))
325
326     @asynchronous
327     @gen.coroutine
328     def put(self, project_name, case_name):
329         """
330         Updates the name and description of a test case
331         :raises HTTPError (HTTP_NOT_FOUND, HTTP_FORBIDDEN)
332         """
333
334         query = {'project_name': project_name, 'name': case_name}
335
336         if self.json_args is None:
337             raise HTTPError(HTTP_BAD_REQUEST, "No payload")
338
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))
345
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")
352
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))
362
363         # new dict for changes
364         request = dict()
365         request = prepare_put_request(request,
366                                       "name",
367                                       new_name,
368                                       testcase.name)
369         request = prepare_put_request(request,
370                                       "project_name",
371                                       new_project_name,
372                                       testcase.project_name)
373         request = prepare_put_request(request,
374                                       "description",
375                                       new_description,
376                                       testcase.description)
377
378         # we raise an exception if there isn't a change
379         if not request:
380             raise HTTPError(HTTP_FORBIDDEN,
381                             "Nothing to update")
382
383         # we merge the whole document """
384         edit_request = testcase.format()
385         edit_request.update(request)
386
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))
391
392     @asynchronous
393     @gen.coroutine
394     def delete(self, project_name, case_name):
395         """ Remove a test case"""
396
397         query = {'project_name': project_name, 'name': case_name}
398
399         # check for an existing case to be deleted
400         testcase = yield self.db.testcases.find_one(query)
401         if testcase is None:
402             raise HTTPError(HTTP_NOT_FOUND,
403                             "{}/{} could not be found as a case to be deleted"
404                             .format(project_name, case_name))
405
406         # just delete it, or maybe save it elsewhere in a future
407         yield self.db.testcases.remove(query)
408         self.finish_request()
409
410
411 class TestResultsHandler(GenericApiHandler):
412     """
413     TestResultsHandler Class
414     Handle the requests about the Test project's results
415     HTTP Methdods :
416         - GET : Get all test results and details about a specific one
417         - POST : Add a test results
418         - DELETE : Remove a test result
419     """
420
421     def initialize(self):
422         """ Prepares the database for the entire class """
423         super(TestResultsHandler, self).initialize()
424         self.name = "test_result"
425
426     @asynchronous
427     @gen.coroutine
428     def get(self, result_id=None):
429         """
430         Retrieve result(s) for a test project on a specific POD.
431         Available filters for this request are :
432          - project : project name
433          - case : case name
434          - pod : pod 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
443
444
445         :param result_id: Get a result by ID
446         :raise HTTPError
447
448         GET /results/project=functest&case=vPing&version=Arno-R1 \
449         &pod=pod_name&period=15
450         => get results with optional filters
451         """
452
453         # prepare request
454         query = dict()
455         if result_id is not None:
456             query["_id"] = result_id
457             answer = yield self.db.results.find_one(query)
458             if answer is None:
459                 raise HTTPError(HTTP_NOT_FOUND,
460                                 "test result {} Not Exist".format(result_id))
461             else:
462                 answer = format_data(answer, TestResult)
463         else:
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",
474                                                           None)
475
476             if project_arg is not None:
477                 query["project_name"] = project_arg
478
479             if case_arg is not None:
480                 query["case_name"] = case_arg
481
482             if pod_arg is not None:
483                 query["pod_name"] = pod_arg
484
485             if version_arg is not None:
486                 query["version"] = version_arg
487
488             if installer_arg is not None:
489                 query["installer"] = installer_arg
490
491             if build_tag_arg is not None:
492                 query["build_tag"] = build_tag_arg
493
494             if scenario_arg is not None:
495                 query["scenario"] = scenario_arg
496
497             if criteria_arg is not None:
498                 query["criteria_tag"] = criteria_arg
499
500             if trust_indicator_arg is not None:
501                 query["trust_indicator_arg"] = trust_indicator_arg
502
503             if period_arg is not None:
504                 try:
505                     period_arg = int(period_arg)
506                 except:
507                     raise HTTPError(HTTP_BAD_REQUEST)
508
509                 if period_arg > 0:
510                     period = datetime.now() - timedelta(days=period_arg)
511                     obj = {"$gte": str(period)}
512                     query["creation_date"] = obj
513
514             res = []
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}
519
520         self.finish_request(answer)
521
522     @asynchronous
523     @gen.coroutine
524     def post(self):
525         """
526         Create a new test result
527         :return: status of the request
528         :raise HTTPError
529         """
530
531         # check for request payload
532         if self.json_args is None:
533             raise HTTPError(HTTP_BAD_REQUEST, 'no payload')
534
535         result = TestResult.from_dict(self.json_args)
536
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')
541
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')
545
546         if result.case_name is None:
547             raise HTTPError(HTTP_BAD_REQUEST, 'testcase is not provided')
548
549         # TODO : replace checks with jsonschema
550         # check for pod
551         the_pod = yield self.db.pods.find_one({"name": result.pod_name})
552         if the_pod is None:
553             raise HTTPError(HTTP_NOT_FOUND,
554                             "Could not find POD [{}] "
555                             .format(self.json_args.get("pod_name")))
556
557         # check for project
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))
563
564         # check for testcase
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))
571
572         _id = yield self.db.results.insert(result.format(), check_keys=False)
573
574         self.finish_request(self._create_response(_id))
575
576
577 class DashboardHandler(GenericApiHandler):
578     """
579     DashboardHandler Class
580     Handle the requests about the Test project's results
581     in a dahboard ready format
582     HTTP Methdods :
583         - GET : Get all test results and details about a specific one
584     """
585     def initialize(self):
586         """ Prepares the database for the entire class """
587         super(DashboardHandler, self).initialize()
588         self.name = "dashboard"
589
590     @asynchronous
591     @gen.coroutine
592     def get(self):
593         """
594         Retrieve dashboard ready result(s) for a test project
595         Available filters for this request are :
596          - project : project name
597          - case : case name
598          - pod : pod name
599          - version : platform version (Arno-R1, ...)
600          - installer (fuel, ...)
601          - period : x (x last days)
602
603
604         :param result_id: Get a result by ID
605         :raise HTTPError
606
607         GET /dashboard?project=functest&case=vPing&version=Arno-R1 \
608         &pod=pod_name&period=15
609         => get results with optional filters
610         """
611
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)
618
619         # prepare request
620         query = dict()
621
622         if project_arg is not None:
623             query["project_name"] = project_arg
624
625         if case_arg is not None:
626             query["case_name"] = case_arg
627
628         if pod_arg is not None:
629             query["pod_name"] = pod_arg
630
631         if version_arg is not None:
632             query["version"] = version_arg
633
634         if installer_arg is not None:
635             query["installer"] = installer_arg
636
637         if period_arg is not None:
638             try:
639                 period_arg = int(period_arg)
640             except:
641                 raise HTTPError(HTTP_BAD_REQUEST)
642             if period_arg > 0:
643                 period = datetime.now() - timedelta(days=period_arg)
644                 obj = {"$gte": str(period)}
645                 query["creation_date"] = obj
646
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")
651
652         if not check_dashboard_ready_project(project_arg):
653             raise HTTPError(HTTP_NOT_FOUND,
654                             'Project [{}] not dashboard ready'
655                             .format(project_arg))
656
657         if case_arg is None:
658             raise HTTPError(
659                 HTTP_NOT_FOUND,
660                 'Test case missing for project [{}]'.format(project_arg))
661
662         if not check_dashboard_ready_case(project_arg, case_arg):
663             raise HTTPError(
664                 HTTP_NOT_FOUND,
665                 'Test case [{}] not dashboard ready for project [{}]'
666                 .format(case_arg, project_arg))
667
668         # special case of status for project
669         res = []
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())
675
676         # final response object
677         self.finish_request(get_dashboard_result(project_arg, case_arg, res))