fff16623736e8e7bff33099d6ad3d16af56751db
[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 ##############################################################################
9
10 import json
11
12 from tornado.web import RequestHandler, asynchronous, HTTPError
13 from tornado import gen
14 from datetime import datetime, timedelta
15
16 from models import TestProject, TestCase, TestResult
17 from resources.pod_models import Pod
18 from common.constants import DEFAULT_REPRESENTATION, HTTP_BAD_REQUEST, \
19     HTTP_NOT_FOUND, HTTP_FORBIDDEN
20 from common.config import prepare_put_request
21
22 from dashboard.dashboard_utils import get_dashboard_cases, \
23     check_dashboard_ready_project, check_dashboard_ready_case, \
24     get_dashboard_result
25
26
27 class GenericApiHandler(RequestHandler):
28     """
29     The purpose of this class is to take benefit of inheritance and prepare
30     a set of common functions for
31     the handlers
32     """
33
34     def initialize(self):
35         """ Prepares the database for the entire class """
36         self.db = self.settings["db"]
37
38     def prepare(self):
39         if not (self.request.method == "GET"):
40             if self.request.headers.get("Content-Type") is not None:
41                 if self.request.headers["Content-Type"].startswith(
42                         DEFAULT_REPRESENTATION):
43                     try:
44                         self.json_args = json.loads(self.request.body)
45                     except (ValueError, KeyError, TypeError) as error:
46                         raise HTTPError(HTTP_BAD_REQUEST,
47                                         "Bad Json format [{}]".
48                                         format(error))
49                 else:
50                     self.json_args = None
51
52     def finish_request(self, json_object):
53         self.write(json.dumps(json_object))
54         self.set_header("Content-Type", DEFAULT_REPRESENTATION)
55         self.finish()
56
57
58 class VersionHandler(RequestHandler):
59     """ Display a message for the API version """
60     def get(self):
61         self.write("Collection of test result API, v1")
62
63
64 class PodHandler(GenericApiHandler):
65     """ Handle the requests about the POD Platforms
66     HTTP Methdods :
67         - GET : Get PODS
68         - POST : Create a pod
69         - DELETE : DELETE POD
70     """
71
72     def initialize(self):
73         """ Prepares the database for the entire class """
74         super(PodHandler, self).initialize()
75
76     @asynchronous
77     @gen.coroutine
78     def get(self, pod_name=None):
79         """
80         Get all pods or a single pod
81         :param pod_id:
82         """
83         get_request = dict()
84
85         if pod_name is not None:
86             get_request["name"] = pod_name
87
88         res = []
89         cursor = self.db.pod.find(get_request)
90         while (yield cursor.fetch_next):
91             pod = Pod.pod_from_dict(cursor.next_object())
92             res.append(pod.format())
93
94         meta = dict()
95         meta["total"] = len(res)
96         meta["success"] = True if len(res) > 0 else False
97
98         answer = dict()
99         answer["pods"] = res
100         answer["meta"] = meta
101
102         self.finish_request(answer)
103
104     @asynchronous
105     @gen.coroutine
106     def post(self):
107         """ Create a POD"""
108
109         if self.json_args is None:
110             raise HTTPError(HTTP_BAD_REQUEST)
111
112         query = {"name": self.json_args.get("name")}
113
114         # check for existing name in db
115         mongo_dict = yield self.db.pod.find_one(query)
116         if mongo_dict is not None:
117             raise HTTPError(HTTP_FORBIDDEN,
118                             "{} already exists as a pod".format(
119                                 self.json_args.get("name")))
120
121         pod = Pod.pod_from_dict(self.json_args)
122         pod.creation_date = datetime.now()
123
124         future = self.db.pod.insert(pod.format())
125         result = yield future
126         pod._id = result
127
128         meta = dict()
129         meta["success"] = True
130         meta["uri"] = "/pods/{}".format(pod.name)
131
132         answer = dict()
133         answer["pod"] = pod.format_http()
134         answer["meta"] = meta
135
136         self.finish_request(answer)
137
138     @asynchronous
139     @gen.coroutine
140     def delete(self, pod_name):
141         """ Remove a POD
142
143         # check for an existing pod to be deleted
144         mongo_dict = yield self.db.pod.find_one(
145             {'name': pod_name})
146         pod = TestProject.pod(mongo_dict)
147         if pod is None:
148             raise HTTPError(HTTP_NOT_FOUND,
149                             "{} could not be found as a pod to be deleted"
150                             .format(pod_name))
151
152         # just delete it, or maybe save it elsewhere in a future
153         res = yield self.db.test_projects.remove(
154             {'name': pod_name})
155
156         meta = dict()
157         meta["success"] = True
158         meta["deletion-data"] = res
159
160         answer = dict()
161         answer["meta"] = meta
162
163         self.finish_request(answer)
164         """
165         pass
166
167
168 class TestProjectHandler(GenericApiHandler):
169     """
170     TestProjectHandler Class
171     Handle the requests about the Test projects
172     HTTP Methdods :
173         - GET : Get all test projects and details about a specific one
174         - POST : Add a test project
175         - PUT : Edit test projects information (name and/or description)
176         - DELETE : Remove a test project
177     """
178
179     def initialize(self):
180         """ Prepares the database for the entire class """
181         super(TestProjectHandler, self).initialize()
182
183     @asynchronous
184     @gen.coroutine
185     def get(self, project_name=None):
186         """
187         Get Project(s) info
188         :param project_name:
189         """
190
191         if project_name is None:
192             project_name = ""
193
194         get_request = dict()
195
196         if len(project_name) > 0:
197             get_request["name"] = project_name
198
199         res = []
200         cursor = self.db.test_projects.find(get_request)
201         while (yield cursor.fetch_next):
202             test_project = TestProject.testproject_from_dict(
203                 cursor.next_object())
204             res.append(test_project.format_http())
205
206         meta = dict()
207         meta["total"] = len(res)
208         meta["success"] = True if len(res) > 0 else False
209
210         answer = dict()
211         answer["test_projects"] = res
212         answer["meta"] = meta
213
214         self.finish_request(answer)
215
216     @asynchronous
217     @gen.coroutine
218     def post(self):
219         """ Create a test project"""
220
221         if self.json_args is None:
222             raise HTTPError(HTTP_BAD_REQUEST)
223
224         query = {"name": self.json_args.get("name")}
225
226         # check for name in db
227         mongo_dict = yield self.db.test_projects.find_one(query)
228         if mongo_dict is not None:
229             raise HTTPError(HTTP_FORBIDDEN,
230                             "{} already exists as a project".format(
231                                 self.json_args.get("name")))
232
233         test_project = TestProject.testproject_from_dict(self.json_args)
234         test_project.creation_date = datetime.now()
235
236         future = self.db.test_projects.insert(test_project.format())
237         result = yield future
238         test_project._id = result
239
240         self.finish_request(test_project.format_http())
241
242     @asynchronous
243     @gen.coroutine
244     def put(self, project_name):
245         """ Updates the name and description of a test project"""
246
247         print "PUT request for : {}".format(project_name)
248
249         query = {'name': project_name}
250         mongo_dict = yield self.db.test_projects.find_one(query)
251         test_project = TestProject.testproject_from_dict(mongo_dict)
252         if test_project is None:
253             raise HTTPError(HTTP_NOT_FOUND,
254                             "{} could not be found".format(project_name))
255
256         new_name = self.json_args.get("name")
257         new_description = self.json_args.get("description")
258
259         # check for payload name parameter in db
260         # avoid a request if the project name has not changed in the payload
261         if new_name != test_project.name:
262             mongo_dict = yield self.db.test_projects.find_one(
263                 {"name": new_name})
264             if mongo_dict is not None:
265                 raise HTTPError(HTTP_FORBIDDEN,
266                                 "{} already exists as a project"
267                                 .format(new_name))
268
269         # new dict for changes
270         request = dict()
271         request = prepare_put_request(request,
272                                       "name",
273                                       new_name,
274                                       test_project.name)
275         request = prepare_put_request(request,
276                                       "description",
277                                       new_description,
278                                       test_project.description)
279
280         """ raise exception if there isn't a change """
281         if not request:
282             raise HTTPError(HTTP_FORBIDDEN,
283                             "Nothing to update")
284
285         """ we merge the whole document """
286         edit_request = test_project.format()
287         edit_request.update(request)
288
289         """ Updating the DB """
290         res = yield self.db.test_projects.update({'name': project_name},
291                                                  edit_request)
292         print res
293         edit_request["_id"] = str(test_project._id)
294
295         self.finish_request({"message": "success", "content": edit_request})
296
297     @asynchronous
298     @gen.coroutine
299     def delete(self, project_name):
300         """ Remove a test project"""
301
302         print "DELETE request for : {}".format(project_name)
303
304         # check for an existing project to be deleted
305         mongo_dict = yield self.db.test_projects.find_one(
306             {'name': project_name})
307         test_project = TestProject.testproject_from_dict(mongo_dict)
308         if test_project is None:
309             raise HTTPError(HTTP_NOT_FOUND,
310                             "{} could not be found as a project to be deleted"
311                             .format(project_name))
312
313         # just delete it, or maybe save it elsewhere in a future
314         res = yield self.db.test_projects.remove(
315             {'name': project_name})
316         print res
317
318         self.finish_request({"message": "success"})
319
320
321 class TestCasesHandler(GenericApiHandler):
322     """
323     TestCasesHandler Class
324     Handle the requests about the Test cases for test projects
325     HTTP Methdods :
326         - GET : Get all test cases and details about a specific one
327         - POST : Add a test project
328         - PUT : Edit test projects information (name and/or description)
329     """
330
331     def initialize(self):
332         """ Prepares the database for the entire class """
333         super(TestCasesHandler, self).initialize()
334
335     @asynchronous
336     @gen.coroutine
337     def get(self, project_name, case_name=None):
338         """
339         Get testcases(s) info
340         :param project_name:
341         :param case_name:
342         """
343
344         if case_name is None:
345             case_name = ""
346
347         get_request = dict()
348         get_request["project_name"] = project_name
349
350         if len(case_name) > 0:
351             get_request["name"] = case_name
352
353         res = []
354         cursor = self.db.test_cases.find(get_request)
355         print get_request
356         while (yield cursor.fetch_next):
357                 test_case = TestCase.test_case_from_dict(cursor.next_object())
358                 res.append(test_case.format_http())
359
360         meta = dict()
361         meta["total"] = len(res)
362         meta["success"] = True if len(res) > 0 else False
363
364         answer = dict()
365         answer["test_cases"] = res
366         answer["meta"] = meta
367
368         self.finish_request(answer)
369
370     @asynchronous
371     @gen.coroutine
372     def post(self, project_name):
373         """ Create a test case"""
374
375         print "POST Request for {}".format(project_name)
376
377         if self.json_args is None:
378             raise HTTPError(HTTP_BAD_REQUEST,
379                             "Check your request payload")
380
381         # retrieve test project
382         mongo_dict = yield self.db.test_projects.find_one(
383             {"name": project_name})
384         if mongo_dict is None:
385             raise HTTPError(HTTP_FORBIDDEN,
386                             "Could not find project {}"
387                             .format(project_name))
388
389         # test_project = TestProject.testproject_from_dict(self.json_args)
390
391         case = TestCase.test_case_from_dict(self.json_args)
392         case.project_name = project_name
393         case.creation_date = datetime.now()
394
395         future = self.db.test_cases.insert(case.format())
396         result = yield future
397         case._id = result
398         self.finish_request(case.format_http())
399
400     @asynchronous
401     @gen.coroutine
402     def put(self, project_name, case_name):
403         """
404         Updates the name and description of a test case
405         :raises HTTPError (HTTP_NOT_FOUND, HTTP_FORBIDDEN)
406         """
407
408         print "PUT request for : {}/{}".format(project_name, case_name)
409         case_request = {'project_name': project_name, 'name': case_name}
410
411         # check if there is a case for the project in url parameters
412         mongo_dict = yield self.db.test_cases.find_one(case_request)
413         test_case = TestCase.test_case_from_dict(mongo_dict)
414         if test_case is None:
415             raise HTTPError(HTTP_NOT_FOUND,
416                             "{} could not be found as a {} case to be updated"
417                             .format(case_name, project_name))
418
419         new_name = self.json_args.get("name")
420         new_project_name = self.json_args.get("project_name")
421         new_description = self.json_args.get("description")
422
423         # check if there is not an existing test case
424         # with the name provided in the json payload
425         mongo_dict = yield self.db.test_cases.find_one(
426             {'project_name': new_project_name, 'name': new_name})
427         if mongo_dict is not None:
428             raise HTTPError(HTTP_FORBIDDEN,
429                             "{} already exists as a project"
430                             .format(new_name))
431
432         # new dict for changes
433         request = dict()
434         request = prepare_put_request(request,
435                                       "name",
436                                       new_name,
437                                       test_case.name)
438         request = prepare_put_request(request,
439                                       "project_name",
440                                       new_project_name,
441                                       test_case.project_name)
442         request = prepare_put_request(request,
443                                       "description",
444                                       new_description,
445                                       test_case.description)
446
447         # we raise an exception if there isn't a change
448         if not request:
449             raise HTTPError(HTTP_FORBIDDEN,
450                             "Nothing to update")
451
452         # we merge the whole document """
453         edit_request = test_case.format()
454         edit_request.update(request)
455
456         """ Updating the DB """
457         res = yield self.db.test_cases.update(case_request, edit_request)
458         print res
459         edit_request["_id"] = str(test_case._id)
460
461         self.finish_request({"message": "success", "content": edit_request})
462
463     @asynchronous
464     @gen.coroutine
465     def delete(self, project_name, case_name):
466         """ Remove a test case"""
467
468         print "DELETE request for : {}/{}".format(project_name, case_name)
469         case_request = {'project_name': project_name, 'name': case_name}
470
471         # check for an existing case to be deleted
472         mongo_dict = yield self.db.test_cases.find_one(case_request)
473         test_project = TestProject.testproject_from_dict(mongo_dict)
474         if test_project is None:
475             raise HTTPError(HTTP_NOT_FOUND,
476                             "{}/{} could not be found as a case to be deleted"
477                             .format(project_name, case_name))
478
479         # just delete it, or maybe save it elsewhere in a future
480         res = yield self.db.test_projects.remove(case_request)
481         print res
482
483         self.finish_request({"message": "success"})
484
485
486 class TestResultsHandler(GenericApiHandler):
487     """
488     TestResultsHandler Class
489     Handle the requests about the Test project's results
490     HTTP Methdods :
491         - GET : Get all test results and details about a specific one
492         - POST : Add a test results
493         - DELETE : Remove a test result
494     """
495
496     def initialize(self):
497         """ Prepares the database for the entire class """
498         super(TestResultsHandler, self).initialize()
499         self.name = "test_result"
500
501     @asynchronous
502     @gen.coroutine
503     def get(self, result_id=None):
504         """
505         Retrieve result(s) for a test project on a specific POD.
506         Available filters for this request are :
507          - project : project name
508          - case : case name
509          - pod : pod name
510          - version : platform version (Arno-R1, ...)
511          - installer (fuel, ...)
512          - build_tag : Jenkins build tag name
513          - period : x (x last days)
514          - scenario : the test scenario (previously version)
515          - criteria : the global criteria status passed or failed
516          - trust_indicator : evaluate the stability of the test case to avoid
517          running systematically long and stable test case
518
519
520         :param result_id: Get a result by ID
521         :raise HTTPError
522
523         GET /results/project=functest&case=vPing&version=Arno-R1 \
524         &pod=pod_name&period=15
525         => get results with optional filters
526         """
527
528         project_arg = self.get_query_argument("project", None)
529         case_arg = self.get_query_argument("case", None)
530         pod_arg = self.get_query_argument("pod", None)
531         version_arg = self.get_query_argument("version", None)
532         installer_arg = self.get_query_argument("installer", None)
533         build_tag_arg = self.get_query_argument("build_tag", None)
534         scenario_arg = self.get_query_argument("scenario", None)
535         criteria_arg = self.get_query_argument("criteria", None)
536         period_arg = self.get_query_argument("period", None)
537         trust_indicator_arg = self.get_query_argument("trust_indicator", None)
538
539         # prepare request
540         get_request = dict()
541         if result_id is None:
542             if project_arg is not None:
543                 get_request["project_name"] = project_arg
544
545             if case_arg is not None:
546                 get_request["case_name"] = case_arg
547
548             if pod_arg is not None:
549                 get_request["pod_name"] = pod_arg
550
551             if version_arg is not None:
552                 get_request["version"] = version_arg
553
554             if installer_arg is not None:
555                 get_request["installer"] = installer_arg
556
557             if build_tag_arg is not None:
558                 get_request["build_tag"] = build_tag_arg
559
560             if scenario_arg is not None:
561                 get_request["scenario"] = scenario_arg
562
563             if criteria_arg is not None:
564                 get_request["criteria_tag"] = criteria_arg
565
566             if trust_indicator_arg is not None:
567                 get_request["trust_indicator_arg"] = trust_indicator_arg
568
569             if period_arg is not None:
570                 try:
571                     period_arg = int(period_arg)
572                 except:
573                     raise HTTPError(HTTP_BAD_REQUEST)
574
575                 if period_arg > 0:
576                     period = datetime.now() - timedelta(days=period_arg)
577                     obj = {"$gte": str(period)}
578                     get_request["creation_date"] = obj
579         else:
580             get_request["_id"] = result_id
581
582         print get_request
583         res = []
584         # fetching results
585         cursor = self.db.test_results.find(get_request)
586         while (yield cursor.fetch_next):
587             test_result = TestResult.test_result_from_dict(
588                 cursor.next_object())
589             res.append(test_result.format_http())
590
591         # building meta object
592         meta = dict()
593         meta["total"] = len(res)
594
595         # final response object
596         answer = dict()
597         answer["test_results"] = res
598         answer["meta"] = meta
599         self.finish_request(answer)
600
601     @asynchronous
602     @gen.coroutine
603     def post(self):
604         """
605         Create a new test result
606         :return: status of the request
607         :raise HTTPError
608         """
609
610         # check for request payload
611         if self.json_args is None:
612             raise HTTPError(HTTP_BAD_REQUEST)
613
614         # check for missing parameters in the request payload
615         if self.json_args.get("project_name") is None:
616             raise HTTPError(HTTP_BAD_REQUEST)
617         if self.json_args.get("case_name") is None:
618             raise HTTPError(HTTP_BAD_REQUEST)
619         # check for pod_name instead of id,
620         # keeping id for current implementations
621         if self.json_args.get("pod_name") is None:
622             raise HTTPError(HTTP_BAD_REQUEST)
623
624         # TODO : replace checks with jsonschema
625         # check for project
626         mongo_dict = yield self.db.test_projects.find_one(
627             {"name": self.json_args.get("project_name")})
628         if mongo_dict is None:
629             raise HTTPError(HTTP_NOT_FOUND,
630                             "Could not find project [{}] "
631                             .format(self.json_args.get("project_name")))
632
633         # check for case
634         mongo_dict = yield self.db.test_cases.find_one(
635             {"name": self.json_args.get("case_name")})
636         if mongo_dict is None:
637             raise HTTPError(HTTP_NOT_FOUND,
638                             "Could not find case [{}] "
639                             .format(self.json_args.get("case_name")))
640
641         # check for pod
642         mongo_dict = yield self.db.pod.find_one(
643             {"name": self.json_args.get("pod_name")})
644         if mongo_dict is None:
645             raise HTTPError(HTTP_NOT_FOUND,
646                             "Could not find POD [{}] "
647                             .format(self.json_args.get("pod_name")))
648
649         # convert payload to object
650         test_result = TestResult.test_result_from_dict(self.json_args)
651         test_result.creation_date = datetime.now()
652
653         future = self.db.test_results.insert(test_result.format(),
654                                              check_keys=False)
655         result = yield future
656         test_result._id = result
657
658         self.finish_request(test_result.format_http())
659
660
661 class DashboardHandler(GenericApiHandler):
662     """
663     DashboardHandler Class
664     Handle the requests about the Test project's results
665     in a dahboard ready format
666     HTTP Methdods :
667         - GET : Get all test results and details about a specific one
668     """
669     def initialize(self):
670         """ Prepares the database for the entire class """
671         super(DashboardHandler, self).initialize()
672         self.name = "dashboard"
673
674     @asynchronous
675     @gen.coroutine
676     def get(self, result_id=None):
677         """
678         Retrieve dashboard ready result(s) for a test project
679         Available filters for this request are :
680          - project : project name
681          - case : case name
682          - pod : pod name
683          - version : platform version (Arno-R1, ...)
684          - installer (fuel, ...)
685          - period : x (x last days)
686
687
688         :param result_id: Get a result by ID
689         :raise HTTPError
690
691         GET /dashboard?project=functest&case=vPing&version=Arno-R1 \
692         &pod=pod_name&period=15
693         => get results with optional filters
694         """
695
696         project_arg = self.get_query_argument("project", None)
697         case_arg = self.get_query_argument("case", None)
698         pod_arg = self.get_query_argument("pod", None)
699         version_arg = self.get_query_argument("version", None)
700         installer_arg = self.get_query_argument("installer", None)
701         period_arg = self.get_query_argument("period", None)
702
703         # prepare request
704         get_request = dict()
705
706         # /dashboard?project=<>&pod=<>...
707         if (result_id is None):
708             if project_arg is not None:
709                 get_request["project_name"] = project_arg
710
711             if case_arg is not None:
712                 get_request["case_name"] = case_arg
713
714             if pod_arg is not None:
715                 get_request["pod_name"] = pod_arg
716
717             if version_arg is not None:
718                 get_request["version"] = version_arg
719
720             if installer_arg is not None:
721                 get_request["installer"] = installer_arg
722
723             if period_arg is not None:
724                 try:
725                     period_arg = int(period_arg)
726                 except:
727                     raise HTTPError(HTTP_BAD_REQUEST)
728                 if period_arg > 0:
729                     period = datetime.now() - timedelta(days=period_arg)
730                     obj = {"$gte": str(period)}
731                     get_request["creation_date"] = obj
732         else:
733             get_request["_id"] = result_id
734
735         dashboard = []
736
737         # on /dashboard retrieve the list of projects and testcases
738         # ready for dashboard
739         if project_arg is None:
740             raise HTTPError(HTTP_NOT_FOUND,
741                             "error:Project name missing")
742         elif check_dashboard_ready_project(project_arg, "./dashboard"):
743             res = []
744
745             if case_arg is None:
746                 raise HTTPError(
747                     HTTP_NOT_FOUND,
748                     "error:Test case missing for project " + project_arg)
749
750             # special case of status for project
751             if case_arg == "status":
752                 del get_request["case_name"]
753                 # retention time to be agreed
754                 # last five days by default?
755                 # TODO move to DB
756                 period = datetime.now() - timedelta(days=5)
757                 get_request["creation_date"] = {"$gte": period}
758
759             # fetching results
760             cursor = self.db.test_results.find(get_request)
761             while (yield cursor.fetch_next):
762                 test_result = TestResult.test_result_from_dict(
763                     cursor.next_object())
764                 res.append(test_result.format_http())
765
766             if check_dashboard_ready_case(project_arg, case_arg):
767                 dashboard = get_dashboard_result(project_arg, case_arg, res)
768             else:
769                 raise HTTPError(
770                     HTTP_NOT_FOUND,
771                     "error:" + case_arg +
772                     " test case not case dashboard ready on project " +
773                     project_arg)
774
775         else:
776             dashboard.append(
777                 {"error": "Project not recognized or not dashboard ready"})
778             dashboard.append(
779                 {"Dashboard-ready-projects":
780                     get_dashboard_cases("./dashboard")})
781             raise HTTPError(
782                 HTTP_NOT_FOUND,
783                 "error: no dashboard ready data for this project")
784
785         # fetching results
786         # cursor = self.db.test_results.find(get_request)
787         # while (yield cursor.fetch_next):
788         #    test_result = TestResult.test_result_from_dict(
789         #        cursor.next_object())
790         #    res.append(test_result.format_http())
791
792         # building meta object
793         meta = dict()
794
795         # final response object
796         answer = dict()
797         answer["dashboard"] = dashboard
798         answer["meta"] = meta
799         self.finish_request(answer)