fix bug on period in Test API
[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          - period : x (x last days)
512
513
514         :param result_id: Get a result by ID
515         :raise HTTPError
516
517         GET /results/project=functest&case=vPing&version=Arno-R1 \
518         &pod=pod_name&period=15
519         => get results with optional filters
520         """
521
522         project_arg = self.get_query_argument("project", None)
523         case_arg = self.get_query_argument("case", None)
524         pod_arg = self.get_query_argument("pod", None)
525         version_arg = self.get_query_argument("version", None)
526         installer_arg = self.get_query_argument("installer", None)
527         period_arg = self.get_query_argument("period", None)
528
529         # prepare request
530         get_request = dict()
531         if result_id is None:
532             if project_arg is not None:
533                 get_request["project_name"] = project_arg
534
535             if case_arg is not None:
536                 get_request["case_name"] = case_arg
537
538             if pod_arg is not None:
539                 get_request["pod_name"] = pod_arg
540
541             if version_arg is not None:
542                 get_request["version"] = version_arg
543
544             if installer_arg is not None:
545                 get_request["installer"] = installer_arg
546
547             if period_arg is not None:
548                 try:
549                     period_arg = int(period_arg)
550                 except:
551                     raise HTTPError(HTTP_BAD_REQUEST)
552
553                 if period_arg > 0:
554                     period = datetime.now() - timedelta(days=period_arg)
555                     obj = {"$gte": str(period)}
556                     get_request["creation_date"] = obj
557         else:
558             get_request["_id"] = result_id
559
560         print get_request
561         res = []
562         # fetching results
563         cursor = self.db.test_results.find(get_request)
564         while (yield cursor.fetch_next):
565             test_result = TestResult.test_result_from_dict(
566                 cursor.next_object())
567             res.append(test_result.format_http())
568
569         # building meta object
570         meta = dict()
571         meta["total"] = len(res)
572
573         # final response object
574         answer = dict()
575         answer["test_results"] = res
576         answer["meta"] = meta
577         self.finish_request(answer)
578
579     @asynchronous
580     @gen.coroutine
581     def post(self):
582         """
583         Create a new test result
584         :return: status of the request
585         :raise HTTPError
586         """
587
588         # check for request payload
589         if self.json_args is None:
590             raise HTTPError(HTTP_BAD_REQUEST)
591
592         # check for missing parameters in the request payload
593         if self.json_args.get("project_name") is None:
594             raise HTTPError(HTTP_BAD_REQUEST)
595         if self.json_args.get("case_name") is None:
596             raise HTTPError(HTTP_BAD_REQUEST)
597         # check for pod_name instead of id,
598         # keeping id for current implementations
599         if self.json_args.get("pod_name") is None:
600             raise HTTPError(HTTP_BAD_REQUEST)
601
602         # TODO : replace checks with jsonschema
603         # check for project
604         mongo_dict = yield self.db.test_projects.find_one(
605             {"name": self.json_args.get("project_name")})
606         if mongo_dict is None:
607             raise HTTPError(HTTP_NOT_FOUND,
608                             "Could not find project [{}] "
609                             .format(self.json_args.get("project_name")))
610
611         # check for case
612         mongo_dict = yield self.db.test_cases.find_one(
613             {"name": self.json_args.get("case_name")})
614         if mongo_dict is None:
615             raise HTTPError(HTTP_NOT_FOUND,
616                             "Could not find case [{}] "
617                             .format(self.json_args.get("case_name")))
618
619         # check for pod
620         mongo_dict = yield self.db.pod.find_one(
621             {"name": self.json_args.get("pod_name")})
622         if mongo_dict is None:
623             raise HTTPError(HTTP_NOT_FOUND,
624                             "Could not find POD [{}] "
625                             .format(self.json_args.get("pod_name")))
626
627         # convert payload to object
628         test_result = TestResult.test_result_from_dict(self.json_args)
629         test_result.creation_date = datetime.now()
630
631         future = self.db.test_results.insert(test_result.format(),
632                                              check_keys=False)
633         result = yield future
634         test_result._id = result
635
636         self.finish_request(test_result.format_http())
637
638
639 class DashboardHandler(GenericApiHandler):
640     """
641     DashboardHandler Class
642     Handle the requests about the Test project's results
643     in a dahboard ready format
644     HTTP Methdods :
645         - GET : Get all test results and details about a specific one
646     """
647     def initialize(self):
648         """ Prepares the database for the entire class """
649         super(DashboardHandler, self).initialize()
650         self.name = "dashboard"
651
652     @asynchronous
653     @gen.coroutine
654     def get(self, result_id=None):
655         """
656         Retrieve dashboard ready result(s) for a test project
657         Available filters for this request are :
658          - project : project name
659          - case : case name
660          - pod : pod name
661          - version : platform version (Arno-R1, ...)
662          - installer (fuel, ...)
663          - period : x (x last days)
664
665
666         :param result_id: Get a result by ID
667         :raise HTTPError
668
669         GET /dashboard?project=functest&case=vPing&version=Arno-R1 \
670         &pod=pod_name&period=15
671         => get results with optional filters
672         """
673
674         project_arg = self.get_query_argument("project", None)
675         case_arg = self.get_query_argument("case", None)
676         pod_arg = self.get_query_argument("pod", None)
677         version_arg = self.get_query_argument("version", None)
678         installer_arg = self.get_query_argument("installer", None)
679         period_arg = self.get_query_argument("period", None)
680
681         # prepare request
682         get_request = dict()
683
684         # /dashboard?project=<>&pod=<>...
685         if (result_id is None):
686             if project_arg is not None:
687                 get_request["project_name"] = project_arg
688
689             if case_arg is not None:
690                 get_request["case_name"] = case_arg
691
692             if pod_arg is not None:
693                 get_request["pod_name"] = pod_arg
694
695             if version_arg is not None:
696                 get_request["version"] = version_arg
697
698             if installer_arg is not None:
699                 get_request["installer"] = installer_arg
700
701             if period_arg is not None:
702                 try:
703                     period_arg = int(period_arg)
704                 except:
705                     raise HTTPError(HTTP_BAD_REQUEST)
706                 if period_arg > 0:
707                     period = datetime.now() - timedelta(days=period_arg)
708                     obj = {"$gte": str(period)}
709                     get_request["creation_date"] = obj
710         else:
711             get_request["_id"] = result_id
712
713         dashboard = []
714
715         # on /dashboard retrieve the list of projects and testcases
716         # ready for dashboard
717         if project_arg is None:
718             raise HTTPError(HTTP_NOT_FOUND,
719                             "error:Project name missing")
720         elif check_dashboard_ready_project(project_arg, "./dashboard"):
721             res = []
722
723             if case_arg is None:
724                 raise HTTPError(
725                     HTTP_NOT_FOUND,
726                     "error:Test case missing for project " + project_arg)
727
728             # special case of status for project
729             if case_arg == "status":
730                 del get_request["case_name"]
731                 # retention time to be agreed
732                 # last five days by default?
733                 # TODO move to DB
734                 period = datetime.now() - timedelta(days=5)
735                 get_request["creation_date"] = {"$gte": period}
736
737             # fetching results
738             cursor = self.db.test_results.find(get_request)
739             while (yield cursor.fetch_next):
740                 test_result = TestResult.test_result_from_dict(
741                     cursor.next_object())
742                 res.append(test_result.format_http())
743
744             if check_dashboard_ready_case(project_arg, case_arg):
745                 dashboard = get_dashboard_result(project_arg, case_arg, res)
746             else:
747                 raise HTTPError(
748                     HTTP_NOT_FOUND,
749                     "error:" + case_arg +
750                     " test case not case dashboard ready on project " +
751                     project_arg)
752
753         else:
754             dashboard.append(
755                 {"error": "Project not recognized or not dashboard ready"})
756             dashboard.append(
757                 {"Dashboard-ready-projects":
758                     get_dashboard_cases("./dashboard")})
759             raise HTTPError(
760                 HTTP_NOT_FOUND,
761                 "error: no dashboard ready data for this project")
762
763         # fetching results
764         # cursor = self.db.test_results.find(get_request)
765         # while (yield cursor.fetch_next):
766         #    test_result = TestResult.test_result_from_dict(
767         #        cursor.next_object())
768         #    res.append(test_result.format_http())
769
770         # building meta object
771         meta = dict()
772
773         # final response object
774         answer = dict()
775         answer["dashboard"] = dashboard
776         answer["meta"] = meta
777         self.finish_request(answer)