Merge "Fix "Illegal option -o pipefail" problem and correct the parser path."
[releng.git] / utils / test / result_collection_api / resources / handlers.py
1 ##############################################################################
2 # Copyright (c) 2015 Orange
3 # guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 # feng.xiaowei@zte.com.cn refactor db.pod to db.pods         5-19-2016
9 # feng.xiaowei@zte.com.cn refactor test_project to project   5-19-2016
10 # feng.xiaowei@zte.com.cn refactor response body             5-19-2016
11 # feng.xiaowei@zte.com.cn refactor pod/project response info 5-19-2016
12 # feng.xiaowei@zte.com.cn refactor testcase related handler  5-20-2016
13 # feng.xiaowei@zte.com.cn refactor result related handler    5-23-2016
14 # feng.xiaowei@zte.com.cn refactor dashboard related handler 5-24-2016
15 # feng.xiaowei@zte.com.cn add methods to GenericApiHandler   5-26-2016
16 # feng.xiaowei@zte.com.cn remove PodHandler                  5-26-2016
17 ##############################################################################
18
19 import json
20 from datetime import datetime, timedelta
21
22 from tornado.web import RequestHandler, asynchronous, HTTPError
23 from tornado import gen
24
25 from models import CreateResponse
26 from resources.result_models import TestResult
27 from resources.testcase_models import Testcase
28 from 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     def __init__(self, application, request, **kwargs):
42         super(GenericApiHandler, self).__init__(application, request, **kwargs)
43         self.db = self.settings["db"]
44         self.json_args = None
45         self.table = None
46         self.table_cls = None
47
48     def prepare(self):
49         if self.request.method != "GET" and self.request.method != "DELETE":
50             if self.request.headers.get("Content-Type") is not None:
51                 if self.request.headers["Content-Type"].startswith(
52                         DEFAULT_REPRESENTATION):
53                     try:
54                         self.json_args = json.loads(self.request.body)
55                     except (ValueError, KeyError, TypeError) as error:
56                         raise HTTPError(HTTP_BAD_REQUEST,
57                                         "Bad Json format [{}]".
58                                         format(error))
59
60     def finish_request(self, json_object=None):
61         if json_object:
62             self.write(json.dumps(json_object))
63         self.set_header("Content-Type", DEFAULT_REPRESENTATION)
64         self.finish()
65
66     def _create_response(self, resource):
67         href = self.request.full_url() + '/' + resource
68         return CreateResponse(href=href).format()
69
70     @asynchronous
71     @gen.coroutine
72     def _create(self, error):
73         if self.json_args is None:
74             raise HTTPError(HTTP_BAD_REQUEST, 'no body')
75
76         data = self.table_cls.from_dict(self.json_args)
77         name = data.name
78         if name is None or name == '':
79             raise HTTPError(HTTP_BAD_REQUEST,
80                             '{} name missing'.format(self.table[:-1]))
81
82         exist_data = yield self._eval_db(self.table, 'find_one',
83                                          {"name": name})
84         if exist_data is not None:
85             raise HTTPError(HTTP_FORBIDDEN,
86                             error.format(name, self.table[:-1]))
87         data.creation_date = datetime.now()
88         yield self._eval_db(self.table, 'insert', data.format())
89         self.finish_request(self._create_response(name))
90
91     @asynchronous
92     @gen.coroutine
93     def _list(self, query=None):
94         if query is None:
95             query = {}
96         res = []
97         cursor = self._eval_db(self.table, 'find', query)
98         while (yield cursor.fetch_next):
99             res.append(format_data(cursor.next_object(), self.table_cls))
100         self.finish_request({self.table: res})
101
102     @asynchronous
103     @gen.coroutine
104     def _get_one(self, query):
105         data = yield self._eval_db(self.table, 'find_one', query)
106         if data is None:
107             raise HTTPError(HTTP_NOT_FOUND,
108                             "[{}] not exist in table [{}]"
109                             .format(query, self.table))
110         self.finish_request(format_data(data, self.table_cls))
111
112     @asynchronous
113     @gen.coroutine
114     def _delete(self, query):
115         data = yield self._eval_db(self.table, 'find_one', query)
116         if data is None:
117             raise HTTPError(HTTP_NOT_FOUND,
118                             "[{}] not exit in table [{}]"
119                             .format(query, self.table))
120
121         yield self._eval_db(self.table, 'remove', query)
122         self.finish_request()
123
124     def _eval_db(self, table, method, param):
125         return eval('self.db.%s.%s(param)' % (table, method))
126
127
128 class VersionHandler(GenericApiHandler):
129     """ Display a message for the API version """
130     def get(self):
131         self.finish_request([{'v1': 'basics'}])
132
133
134 class TestcaseHandler(GenericApiHandler):
135     """
136     TestCasesHandler Class
137     Handle the requests about the Test cases for test projects
138     HTTP Methdods :
139         - GET : Get all test cases and details about a specific one
140         - POST : Add a test project
141         - PUT : Edit test projects information (name and/or description)
142     """
143
144     def initialize(self):
145         """ Prepares the database for the entire class """
146         super(TestcaseHandler, self).initialize()
147
148     @asynchronous
149     @gen.coroutine
150     def get(self, project_name, case_name=None):
151         """
152         Get testcases(s) info
153         :param project_name:
154         :param case_name:
155         """
156
157         query = {'project_name': project_name}
158
159         if case_name is not None:
160             query["name"] = case_name
161             answer = yield self.db.testcases.find_one(query)
162             if answer is None:
163                 raise HTTPError(HTTP_NOT_FOUND,
164                                 "{} Not Exist".format(case_name))
165             else:
166                 answer = format_data(answer, Testcase)
167         else:
168             res = []
169             cursor = self.db.testcases.find(query)
170             while (yield cursor.fetch_next):
171                 res.append(format_data(cursor.next_object(), Testcase))
172             answer = {'testcases': res}
173
174         self.finish_request(answer)
175
176     @asynchronous
177     @gen.coroutine
178     def post(self, project_name):
179         """ Create a test case"""
180
181         if self.json_args is None:
182             raise HTTPError(HTTP_BAD_REQUEST,
183                             "Check your request payload")
184
185         # retrieve test project
186         project = yield self.db.projects.find_one(
187             {"name": project_name})
188         if project is None:
189             raise HTTPError(HTTP_FORBIDDEN,
190                             "Could not find project {}"
191                             .format(project_name))
192
193         case_name = self.json_args.get('name')
194         the_testcase = yield self.db.testcases.find_one(
195             {"project_name": project_name, 'name': case_name})
196         if the_testcase:
197             raise HTTPError(HTTP_FORBIDDEN,
198                             "{} already exists as a case in project {}"
199                             .format(case_name, project_name))
200
201         testcase = Testcase.from_dict(self.json_args)
202         testcase.project_name = project_name
203         testcase.creation_date = datetime.now()
204
205         yield self.db.testcases.insert(testcase.format())
206         self.finish_request(self._create_response(testcase.name))
207
208     @asynchronous
209     @gen.coroutine
210     def put(self, project_name, case_name):
211         """
212         Updates the name and description of a test case
213         :raises HTTPError (HTTP_NOT_FOUND, HTTP_FORBIDDEN)
214         """
215
216         query = {'project_name': project_name, 'name': case_name}
217
218         if self.json_args is None:
219             raise HTTPError(HTTP_BAD_REQUEST, "No payload")
220
221         # check if there is a case for the project in url parameters
222         from_testcase = yield self.db.testcases.find_one(query)
223         if from_testcase is None:
224             raise HTTPError(HTTP_NOT_FOUND,
225                             "{} could not be found as a {} case to be updated"
226                             .format(case_name, project_name))
227
228         testcase = Testcase.from_dict(from_testcase)
229         new_name = self.json_args.get("name")
230         new_project_name = self.json_args.get("project_name")
231         if not new_project_name:
232             new_project_name = project_name
233         new_description = self.json_args.get("description")
234
235         # check if there is not an existing test case
236         # with the name provided in the json payload
237         if new_name != case_name or new_project_name != project_name:
238             to_testcase = yield self.db.testcases.find_one(
239                 {'project_name': new_project_name, 'name': new_name})
240             if to_testcase is not None:
241                 raise HTTPError(HTTP_FORBIDDEN,
242                                 "{} already exists as a case in project"
243                                 .format(new_name, new_project_name))
244
245         # new dict for changes
246         request = dict()
247         request = prepare_put_request(request,
248                                       "name",
249                                       new_name,
250                                       testcase.name)
251         request = prepare_put_request(request,
252                                       "project_name",
253                                       new_project_name,
254                                       testcase.project_name)
255         request = prepare_put_request(request,
256                                       "description",
257                                       new_description,
258                                       testcase.description)
259
260         # we raise an exception if there isn't a change
261         if not request:
262             raise HTTPError(HTTP_FORBIDDEN,
263                             "Nothing to update")
264
265         # we merge the whole document """
266         edit_request = testcase.format()
267         edit_request.update(request)
268
269         """ Updating the DB """
270         yield self.db.testcases.update(query, edit_request)
271         new_case = yield self.db.testcases.find_one({"_id": testcase._id})
272         self.finish_request(format_data(new_case, Testcase))
273
274     @asynchronous
275     @gen.coroutine
276     def delete(self, project_name, case_name):
277         """ Remove a test case"""
278
279         query = {'project_name': project_name, 'name': case_name}
280
281         # check for an existing case to be deleted
282         testcase = yield self.db.testcases.find_one(query)
283         if testcase is None:
284             raise HTTPError(HTTP_NOT_FOUND,
285                             "{}/{} could not be found as a case to be deleted"
286                             .format(project_name, case_name))
287
288         # just delete it, or maybe save it elsewhere in a future
289         yield self.db.testcases.remove(query)
290         self.finish_request()
291
292
293 class TestResultsHandler(GenericApiHandler):
294     """
295     TestResultsHandler Class
296     Handle the requests about the Test project's results
297     HTTP Methdods :
298         - GET : Get all test results and details about a specific one
299         - POST : Add a test results
300         - DELETE : Remove a test result
301     """
302
303     def initialize(self):
304         """ Prepares the database for the entire class """
305         super(TestResultsHandler, self).initialize()
306         self.name = "test_result"
307
308     @asynchronous
309     @gen.coroutine
310     def get(self, result_id=None):
311         """
312         Retrieve result(s) for a test project on a specific POD.
313         Available filters for this request are :
314          - project : project name
315          - case : case name
316          - pod : pod name
317          - version : platform version (Arno-R1, ...)
318          - installer (fuel, ...)
319          - build_tag : Jenkins build tag name
320          - period : x (x last days)
321          - scenario : the test scenario (previously version)
322          - criteria : the global criteria status passed or failed
323          - trust_indicator : evaluate the stability of the test case to avoid
324          running systematically long and stable test case
325
326
327         :param result_id: Get a result by ID
328         :raise HTTPError
329
330         GET /results/project=functest&case=vPing&version=Arno-R1 \
331         &pod=pod_name&period=15
332         => get results with optional filters
333         """
334
335         # prepare request
336         query = dict()
337         if result_id is not None:
338             query["_id"] = result_id
339             answer = yield self.db.results.find_one(query)
340             if answer is None:
341                 raise HTTPError(HTTP_NOT_FOUND,
342                                 "test result {} Not Exist".format(result_id))
343             else:
344                 answer = format_data(answer, TestResult)
345         else:
346             pod_arg = self.get_query_argument("pod", None)
347             project_arg = self.get_query_argument("project", None)
348             case_arg = self.get_query_argument("case", None)
349             version_arg = self.get_query_argument("version", None)
350             installer_arg = self.get_query_argument("installer", None)
351             build_tag_arg = self.get_query_argument("build_tag", None)
352             scenario_arg = self.get_query_argument("scenario", None)
353             criteria_arg = self.get_query_argument("criteria", None)
354             period_arg = self.get_query_argument("period", None)
355             trust_indicator_arg = self.get_query_argument("trust_indicator",
356                                                           None)
357
358             if project_arg is not None:
359                 query["project_name"] = project_arg
360
361             if case_arg is not None:
362                 query["case_name"] = case_arg
363
364             if pod_arg is not None:
365                 query["pod_name"] = pod_arg
366
367             if version_arg is not None:
368                 query["version"] = version_arg
369
370             if installer_arg is not None:
371                 query["installer"] = installer_arg
372
373             if build_tag_arg is not None:
374                 query["build_tag"] = build_tag_arg
375
376             if scenario_arg is not None:
377                 query["scenario"] = scenario_arg
378
379             if criteria_arg is not None:
380                 query["criteria_tag"] = criteria_arg
381
382             if trust_indicator_arg is not None:
383                 query["trust_indicator_arg"] = trust_indicator_arg
384
385             if period_arg is not None:
386                 try:
387                     period_arg = int(period_arg)
388                 except:
389                     raise HTTPError(HTTP_BAD_REQUEST)
390
391                 if period_arg > 0:
392                     period = datetime.now() - timedelta(days=period_arg)
393                     obj = {"$gte": str(period)}
394                     query["creation_date"] = obj
395
396             res = []
397             cursor = self.db.results.find(query)
398             while (yield cursor.fetch_next):
399                 res.append(format_data(cursor.next_object(), TestResult))
400             answer = {'results': res}
401
402         self.finish_request(answer)
403
404     @asynchronous
405     @gen.coroutine
406     def post(self):
407         """
408         Create a new test result
409         :return: status of the request
410         :raise HTTPError
411         """
412
413         # check for request payload
414         if self.json_args is None:
415             raise HTTPError(HTTP_BAD_REQUEST, 'no payload')
416
417         result = TestResult.from_dict(self.json_args)
418
419         # check for pod_name instead of id,
420         # keeping id for current implementations
421         if result.pod_name is None:
422             raise HTTPError(HTTP_BAD_REQUEST, 'pod is not provided')
423
424         # check for missing parameters in the request payload
425         if result.project_name is None:
426             raise HTTPError(HTTP_BAD_REQUEST, 'project is not provided')
427
428         if result.case_name is None:
429             raise HTTPError(HTTP_BAD_REQUEST, 'testcase is not provided')
430
431         # TODO : replace checks with jsonschema
432         # check for pod
433         the_pod = yield self.db.pods.find_one({"name": result.pod_name})
434         if the_pod is None:
435             raise HTTPError(HTTP_NOT_FOUND,
436                             "Could not find POD [{}] "
437                             .format(self.json_args.get("pod_name")))
438
439         # check for project
440         the_project = yield self.db.projects.find_one(
441             {"name": result.project_name})
442         if the_project is None:
443             raise HTTPError(HTTP_NOT_FOUND, "Could not find project [{}] "
444                             .format(result.project_name))
445
446         # check for testcase
447         the_testcase = yield self.db.testcases.find_one(
448             {"name": result.case_name})
449         if the_testcase is None:
450             raise HTTPError(HTTP_NOT_FOUND,
451                             "Could not find testcase [{}] "
452                             .format(result.case_name))
453
454         _id = yield self.db.results.insert(result.format(), check_keys=False)
455
456         self.finish_request(self._create_response(_id))
457
458
459 class DashboardHandler(GenericApiHandler):
460     """
461     DashboardHandler Class
462     Handle the requests about the Test project's results
463     in a dahboard ready format
464     HTTP Methdods :
465         - GET : Get all test results and details about a specific one
466     """
467     def initialize(self):
468         """ Prepares the database for the entire class """
469         super(DashboardHandler, self).initialize()
470         self.name = "dashboard"
471
472     @asynchronous
473     @gen.coroutine
474     def get(self):
475         """
476         Retrieve dashboard ready result(s) for a test project
477         Available filters for this request are :
478          - project : project name
479          - case : case name
480          - pod : pod name
481          - version : platform version (Arno-R1, ...)
482          - installer (fuel, ...)
483          - period : x (x last days)
484
485
486         :param result_id: Get a result by ID
487         :raise HTTPError
488
489         GET /dashboard?project=functest&case=vPing&version=Arno-R1 \
490         &pod=pod_name&period=15
491         => get results with optional filters
492         """
493
494         project_arg = self.get_query_argument("project", None)
495         case_arg = self.get_query_argument("case", None)
496         pod_arg = self.get_query_argument("pod", None)
497         version_arg = self.get_query_argument("version", None)
498         installer_arg = self.get_query_argument("installer", None)
499         period_arg = self.get_query_argument("period", None)
500
501         # prepare request
502         query = dict()
503
504         if project_arg is not None:
505             query["project_name"] = project_arg
506
507         if case_arg is not None:
508             query["case_name"] = case_arg
509
510         if pod_arg is not None:
511             query["pod_name"] = pod_arg
512
513         if version_arg is not None:
514             query["version"] = version_arg
515
516         if installer_arg is not None:
517             query["installer"] = installer_arg
518
519         if period_arg is not None:
520             try:
521                 period_arg = int(period_arg)
522             except:
523                 raise HTTPError(HTTP_BAD_REQUEST)
524             if period_arg > 0:
525                 period = datetime.now() - timedelta(days=period_arg)
526                 obj = {"$gte": str(period)}
527                 query["creation_date"] = obj
528
529         # on /dashboard retrieve the list of projects and testcases
530         # ready for dashboard
531         if project_arg is None:
532             raise HTTPError(HTTP_NOT_FOUND, "Project name missing")
533
534         if not check_dashboard_ready_project(project_arg):
535             raise HTTPError(HTTP_NOT_FOUND,
536                             'Project [{}] not dashboard ready'
537                             .format(project_arg))
538
539         if case_arg is None:
540             raise HTTPError(
541                 HTTP_NOT_FOUND,
542                 'Test case missing for project [{}]'.format(project_arg))
543
544         if not check_dashboard_ready_case(project_arg, case_arg):
545             raise HTTPError(
546                 HTTP_NOT_FOUND,
547                 'Test case [{}] not dashboard ready for project [{}]'
548                 .format(case_arg, project_arg))
549
550         # special case of status for project
551         res = []
552         if case_arg != "status":
553             cursor = self.db.results.find(query)
554             while (yield cursor.fetch_next):
555                 result = TestResult.from_dict(cursor.next_object())
556                 res.append(result.format_http())
557
558         # final response object
559         self.finish_request(get_dashboard_result(project_arg, case_arg, res))