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