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