7d2e8161e11b4e8702dc4a93a01f6917ef0429d5
[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 common.constants import DEFAULT_REPRESENTATION, HTTP_BAD_REQUEST, \
28     HTTP_NOT_FOUND, HTTP_FORBIDDEN
29 from common.config import prepare_put_request
30 from dashboard.dashboard_utils import check_dashboard_ready_project, \
31     check_dashboard_ready_case, get_dashboard_result
32
33
34 def format_data(data, cls):
35     cls_data = cls.from_dict(data)
36     return cls_data.format_http()
37
38
39 class GenericApiHandler(RequestHandler):
40     def __init__(self, application, request, **kwargs):
41         super(GenericApiHandler, self).__init__(application, request, **kwargs)
42         self.db = self.settings["db"]
43         self.json_args = None
44         self.table = None
45         self.table_cls = None
46         self.db_projects = 'projects'
47         self.db_pods = 'pods'
48         self.db_testcases = 'testcases'
49         self.db_results = 'results'
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
63     def finish_request(self, json_object=None):
64         if json_object:
65             self.write(json.dumps(json_object))
66         self.set_header("Content-Type", DEFAULT_REPRESENTATION)
67         self.finish()
68
69     def _create_response(self, resource):
70         href = self.request.full_url() + '/' + resource
71         return CreateResponse(href=href).format()
72
73     @asynchronous
74     @gen.coroutine
75     def _create(self, db_checks, **kwargs):
76         """
77         :param db_checks: [(table, exist, query, (error, message))]
78         :param db_op: (insert/remove)
79         :param res_op: (_create_response/None)
80         :return:
81         """
82         if self.json_args is None:
83             raise HTTPError(HTTP_BAD_REQUEST, "no body")
84
85         data = self.table_cls.from_dict(self.json_args)
86         name = data.name
87         if name is None or name == '':
88             raise HTTPError(HTTP_BAD_REQUEST,
89                             '{} name missing'.format(self.table[:-1]))
90
91         for k, v in kwargs.iteritems():
92             data.__setattr__(k, v)
93
94         for table, exist, query, error in db_checks:
95             check = yield self._eval_db(table, 'find_one', query(data))
96             if (exist and not check) or (not exist and check):
97                 code, message = error(data)
98                 raise HTTPError(code, message)
99
100         data.creation_date = datetime.now()
101         yield self._eval_db(self.table, 'insert', data.format())
102         self.finish_request(self._create_response(name))
103
104     @asynchronous
105     @gen.coroutine
106     def _list(self, query=None):
107         if query is None:
108             query = {}
109         res = []
110         cursor = self._eval_db(self.table, 'find', query)
111         while (yield cursor.fetch_next):
112             res.append(format_data(cursor.next_object(), self.table_cls))
113         self.finish_request({self.table: res})
114
115     @asynchronous
116     @gen.coroutine
117     def _get_one(self, query):
118         data = yield self._eval_db(self.table, 'find_one', query)
119         if data is None:
120             raise HTTPError(HTTP_NOT_FOUND,
121                             "[{}] not exist in table [{}]"
122                             .format(query, self.table))
123         self.finish_request(format_data(data, self.table_cls))
124
125     @asynchronous
126     @gen.coroutine
127     def _delete(self, query):
128         data = yield self._eval_db(self.table, 'find_one', query)
129         if data is None:
130             raise HTTPError(HTTP_NOT_FOUND,
131                             "[{}] not exit in table [{}]"
132                             .format(query, self.table))
133
134         yield self._eval_db(self.table, 'remove', query)
135         self.finish_request()
136
137     @asynchronous
138     @gen.coroutine
139     def _update(self, query, db_keys):
140         if self.json_args is None:
141             raise HTTPError(HTTP_BAD_REQUEST, "No payload")
142
143         # check old data exist
144         from_data = yield self._eval_db(self.table, 'find_one', query)
145         if from_data is None:
146             raise HTTPError(HTTP_NOT_FOUND,
147                             "{} could not be found in table [{}]"
148                             .format(query, self.table))
149
150         data = self.table_cls.from_dict(from_data)
151         # check new data exist
152         equal, new_query = self._update_query(db_keys, data)
153         if not equal:
154             to_data = yield self._eval_db(self.table, 'find_one', new_query)
155             if to_data is not None:
156                 raise HTTPError(HTTP_FORBIDDEN,
157                                 "{} already exists in table [{}]"
158                                 .format(new_query, self.table))
159
160         # we merge the whole document """
161         edit_request = data.format()
162         edit_request.update(self._update_request(data))
163
164         """ Updating the DB """
165         yield self._eval_db(self.table, 'update', query, edit_request)
166         edit_request['_id'] = str(data._id)
167         self.finish_request(edit_request)
168
169     def _update_request(self, data):
170         request = dict()
171         for k, v in self.json_args.iteritems():
172             request = prepare_put_request(request, k, v,
173                                           data.__getattribute__(k))
174         if not request:
175             raise HTTPError(HTTP_FORBIDDEN, "Nothing to update")
176         return request
177
178     def _update_query(self, keys, data):
179         query = dict()
180         equal = True
181         for key in keys:
182             new = self.json_args.get(key)
183             old = data.__getattribute__(key)
184             if new is None:
185                 new = old
186             elif new != old:
187                 equal = False
188             query[key] = new
189         return equal, query
190
191     def _eval_db(self, table, method, *args):
192         return eval('self.db.%s.%s(*args)' % (table, method))
193
194
195 class VersionHandler(GenericApiHandler):
196     """ Display a message for the API version """
197     def get(self):
198         self.finish_request([{'v1': 'basics'}])
199
200
201 class TestResultsHandler(GenericApiHandler):
202     """
203     TestResultsHandler Class
204     Handle the requests about the Test project's results
205     HTTP Methdods :
206         - GET : Get all test results and details about a specific one
207         - POST : Add a test results
208         - DELETE : Remove a test result
209     """
210
211     def initialize(self):
212         """ Prepares the database for the entire class """
213         super(TestResultsHandler, self).initialize()
214         self.name = "test_result"
215
216     @asynchronous
217     @gen.coroutine
218     def get(self, result_id=None):
219         """
220         Retrieve result(s) for a test project on a specific POD.
221         Available filters for this request are :
222          - project : project name
223          - case : case name
224          - pod : pod name
225          - version : platform version (Arno-R1, ...)
226          - installer (fuel, ...)
227          - build_tag : Jenkins build tag name
228          - period : x (x last days)
229          - scenario : the test scenario (previously version)
230          - criteria : the global criteria status passed or failed
231          - trust_indicator : evaluate the stability of the test case to avoid
232          running systematically long and stable test case
233
234
235         :param result_id: Get a result by ID
236         :raise HTTPError
237
238         GET /results/project=functest&case=vPing&version=Arno-R1 \
239         &pod=pod_name&period=15
240         => get results with optional filters
241         """
242
243         # prepare request
244         query = dict()
245         if result_id is not None:
246             query["_id"] = result_id
247             answer = yield self.db.results.find_one(query)
248             if answer is None:
249                 raise HTTPError(HTTP_NOT_FOUND,
250                                 "test result {} Not Exist".format(result_id))
251             else:
252                 answer = format_data(answer, TestResult)
253         else:
254             pod_arg = self.get_query_argument("pod", None)
255             project_arg = self.get_query_argument("project", None)
256             case_arg = self.get_query_argument("case", None)
257             version_arg = self.get_query_argument("version", None)
258             installer_arg = self.get_query_argument("installer", None)
259             build_tag_arg = self.get_query_argument("build_tag", None)
260             scenario_arg = self.get_query_argument("scenario", None)
261             criteria_arg = self.get_query_argument("criteria", None)
262             period_arg = self.get_query_argument("period", None)
263             trust_indicator_arg = self.get_query_argument("trust_indicator",
264                                                           None)
265
266             if project_arg is not None:
267                 query["project_name"] = project_arg
268
269             if case_arg is not None:
270                 query["case_name"] = case_arg
271
272             if pod_arg is not None:
273                 query["pod_name"] = pod_arg
274
275             if version_arg is not None:
276                 query["version"] = version_arg
277
278             if installer_arg is not None:
279                 query["installer"] = installer_arg
280
281             if build_tag_arg is not None:
282                 query["build_tag"] = build_tag_arg
283
284             if scenario_arg is not None:
285                 query["scenario"] = scenario_arg
286
287             if criteria_arg is not None:
288                 query["criteria_tag"] = criteria_arg
289
290             if trust_indicator_arg is not None:
291                 query["trust_indicator_arg"] = trust_indicator_arg
292
293             if period_arg is not None:
294                 try:
295                     period_arg = int(period_arg)
296                 except:
297                     raise HTTPError(HTTP_BAD_REQUEST)
298
299                 if period_arg > 0:
300                     period = datetime.now() - timedelta(days=period_arg)
301                     obj = {"$gte": str(period)}
302                     query["creation_date"] = obj
303
304             res = []
305             cursor = self.db.results.find(query)
306             while (yield cursor.fetch_next):
307                 res.append(format_data(cursor.next_object(), TestResult))
308             answer = {'results': res}
309
310         self.finish_request(answer)
311
312     @asynchronous
313     @gen.coroutine
314     def post(self):
315         """
316         Create a new test result
317         :return: status of the request
318         :raise HTTPError
319         """
320
321         # check for request payload
322         if self.json_args is None:
323             raise HTTPError(HTTP_BAD_REQUEST, 'no payload')
324
325         result = TestResult.from_dict(self.json_args)
326
327         # check for pod_name instead of id,
328         # keeping id for current implementations
329         if result.pod_name is None:
330             raise HTTPError(HTTP_BAD_REQUEST, 'pod is not provided')
331
332         # check for missing parameters in the request payload
333         if result.project_name is None:
334             raise HTTPError(HTTP_BAD_REQUEST, 'project is not provided')
335
336         if result.case_name is None:
337             raise HTTPError(HTTP_BAD_REQUEST, 'testcase is not provided')
338
339         # TODO : replace checks with jsonschema
340         # check for pod
341         the_pod = yield self.db.pods.find_one({"name": result.pod_name})
342         if the_pod is None:
343             raise HTTPError(HTTP_NOT_FOUND,
344                             "Could not find POD [{}] "
345                             .format(self.json_args.get("pod_name")))
346
347         # check for project
348         the_project = yield self.db.projects.find_one(
349             {"name": result.project_name})
350         if the_project is None:
351             raise HTTPError(HTTP_NOT_FOUND, "Could not find project [{}] "
352                             .format(result.project_name))
353
354         # check for testcase
355         the_testcase = yield self.db.testcases.find_one(
356             {"name": result.case_name})
357         if the_testcase is None:
358             raise HTTPError(HTTP_NOT_FOUND,
359                             "Could not find testcase [{}] "
360                             .format(result.case_name))
361
362         _id = yield self.db.results.insert(result.format(), check_keys=False)
363
364         self.finish_request(self._create_response(_id))
365
366
367 class DashboardHandler(GenericApiHandler):
368     """
369     DashboardHandler Class
370     Handle the requests about the Test project's results
371     in a dahboard ready format
372     HTTP Methdods :
373         - GET : Get all test results and details about a specific one
374     """
375     def initialize(self):
376         """ Prepares the database for the entire class """
377         super(DashboardHandler, self).initialize()
378         self.name = "dashboard"
379
380     @asynchronous
381     @gen.coroutine
382     def get(self):
383         """
384         Retrieve dashboard ready result(s) for a test project
385         Available filters for this request are :
386          - project : project name
387          - case : case name
388          - pod : pod name
389          - version : platform version (Arno-R1, ...)
390          - installer (fuel, ...)
391          - period : x (x last days)
392
393
394         :param result_id: Get a result by ID
395         :raise HTTPError
396
397         GET /dashboard?project=functest&case=vPing&version=Arno-R1 \
398         &pod=pod_name&period=15
399         => get results with optional filters
400         """
401
402         project_arg = self.get_query_argument("project", None)
403         case_arg = self.get_query_argument("case", None)
404         pod_arg = self.get_query_argument("pod", None)
405         version_arg = self.get_query_argument("version", None)
406         installer_arg = self.get_query_argument("installer", None)
407         period_arg = self.get_query_argument("period", None)
408
409         # prepare request
410         query = dict()
411
412         if project_arg is not None:
413             query["project_name"] = project_arg
414
415         if case_arg is not None:
416             query["case_name"] = case_arg
417
418         if pod_arg is not None:
419             query["pod_name"] = pod_arg
420
421         if version_arg is not None:
422             query["version"] = version_arg
423
424         if installer_arg is not None:
425             query["installer"] = installer_arg
426
427         if period_arg is not None:
428             try:
429                 period_arg = int(period_arg)
430             except:
431                 raise HTTPError(HTTP_BAD_REQUEST)
432             if period_arg > 0:
433                 period = datetime.now() - timedelta(days=period_arg)
434                 obj = {"$gte": str(period)}
435                 query["creation_date"] = obj
436
437         # on /dashboard retrieve the list of projects and testcases
438         # ready for dashboard
439         if project_arg is None:
440             raise HTTPError(HTTP_NOT_FOUND, "Project name missing")
441
442         if not check_dashboard_ready_project(project_arg):
443             raise HTTPError(HTTP_NOT_FOUND,
444                             'Project [{}] not dashboard ready'
445                             .format(project_arg))
446
447         if case_arg is None:
448             raise HTTPError(
449                 HTTP_NOT_FOUND,
450                 'Test case missing for project [{}]'.format(project_arg))
451
452         if not check_dashboard_ready_case(project_arg, case_arg):
453             raise HTTPError(
454                 HTTP_NOT_FOUND,
455                 'Test case [{}] not dashboard ready for project [{}]'
456                 .format(case_arg, project_arg))
457
458         # special case of status for project
459         res = []
460         if case_arg != "status":
461             cursor = self.db.results.find(query)
462             while (yield cursor.fetch_next):
463                 result = TestResult.from_dict(cursor.next_object())
464                 res.append(result.format_http())
465
466         # final response object
467         self.finish_request(get_dashboard_result(project_arg, case_arg, res))