##// END OF EJS Templates
api: added store_exception_api for remote exception storage....
marcink -
r3317:a029d28f default
parent child Browse files
Show More
@@ -0,0 +1,59 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import pytest
23
24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok, assert_error
25
26
27 @pytest.mark.usefixtures("testuser_api", "app")
28 class TestStoreException(object):
29
30 def test_store_exception_invalid_json(self):
31 id_, params = build_data(self.apikey, 'store_exception',
32 exc_data_json='XXX,{')
33 response = api_call(self.app, params)
34
35 expected = 'Failed to parse JSON data from exc_data_json field. ' \
36 'Please make sure it contains a valid JSON.'
37 assert_error(id_, expected, given=response.body)
38
39 def test_store_exception_missing_json_params_json(self):
40 id_, params = build_data(self.apikey, 'store_exception',
41 exc_data_json='{"foo":"bar"}')
42 response = api_call(self.app, params)
43
44 expected = "Missing exc_traceback, or exc_type_name in " \
45 "exc_data_json field. Missing: 'exc_traceback'"
46 assert_error(id_, expected, given=response.body)
47
48 def test_store_exception(self):
49 id_, params = build_data(
50 self.apikey, 'store_exception',
51 exc_data_json='{"exc_traceback": "invalid", "exc_type_name":"ValueError"}')
52 response = api_call(self.app, params)
53 exc_id = response.json['result']['exc_id']
54
55 expected = {
56 'exc_id': exc_id,
57 'exc_url': 'http://example.com/_admin/settings/exceptions/{}'.format(exc_id)
58 }
59 assert_ok(id_, expected, given=response.body)
@@ -1,351 +1,414 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import inspect
22 22 import logging
23 23 import itertools
24 24
25 25 from rhodecode.api import (
26 26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
27 27
28 28 from rhodecode.api.utils import (
29 29 Optional, OAttr, has_superadmin_permission, get_user_or_error)
30 30 from rhodecode.lib.utils import repo2db_mapper
31 31 from rhodecode.lib import system_info
32 32 from rhodecode.lib import user_sessions
33 from rhodecode.lib import exc_tracking
34 from rhodecode.lib.ext_json import json
33 35 from rhodecode.lib.utils2 import safe_int
34 36 from rhodecode.model.db import UserIpMap
35 37 from rhodecode.model.scm import ScmModel
36 38 from rhodecode.model.settings import VcsSettingsModel
37 39
38 40 log = logging.getLogger(__name__)
39 41
40 42
41 43 @jsonrpc_method()
42 44 def get_server_info(request, apiuser):
43 45 """
44 46 Returns the |RCE| server information.
45 47
46 48 This includes the running version of |RCE| and all installed
47 49 packages. This command takes the following options:
48 50
49 51 :param apiuser: This is filled automatically from the |authtoken|.
50 52 :type apiuser: AuthUser
51 53
52 54 Example output:
53 55
54 56 .. code-block:: bash
55 57
56 58 id : <id_given_in_input>
57 59 result : {
58 60 'modules': [<module name>,...]
59 61 'py_version': <python version>,
60 62 'platform': <platform type>,
61 63 'rhodecode_version': <rhodecode version>
62 64 }
63 65 error : null
64 66 """
65 67
66 68 if not has_superadmin_permission(apiuser):
67 69 raise JSONRPCForbidden()
68 70
69 71 server_info = ScmModel().get_server_info(request.environ)
70 72 # rhodecode-index requires those
71 73
72 74 server_info['index_storage'] = server_info['search']['value']['location']
73 75 server_info['storage'] = server_info['storage']['value']['path']
74 76
75 77 return server_info
76 78
77 79
78 80 @jsonrpc_method()
79 81 def get_repo_store(request, apiuser):
80 82 """
81 83 Returns the |RCE| repository storage information.
82 84
83 85 :param apiuser: This is filled automatically from the |authtoken|.
84 86 :type apiuser: AuthUser
85 87
86 88 Example output:
87 89
88 90 .. code-block:: bash
89 91
90 92 id : <id_given_in_input>
91 93 result : {
92 94 'modules': [<module name>,...]
93 95 'py_version': <python version>,
94 96 'platform': <platform type>,
95 97 'rhodecode_version': <rhodecode version>
96 98 }
97 99 error : null
98 100 """
99 101
100 102 if not has_superadmin_permission(apiuser):
101 103 raise JSONRPCForbidden()
102 104
103 105 path = VcsSettingsModel().get_repos_location()
104 106 return {"path": path}
105 107
106 108
107 109 @jsonrpc_method()
108 110 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
109 111 """
110 112 Displays the IP Address as seen from the |RCE| server.
111 113
112 114 * This command displays the IP Address, as well as all the defined IP
113 115 addresses for the specified user. If the ``userid`` is not set, the
114 116 data returned is for the user calling the method.
115 117
116 118 This command can only be run using an |authtoken| with admin rights to
117 119 the specified repository.
118 120
119 121 This command takes the following options:
120 122
121 123 :param apiuser: This is filled automatically from |authtoken|.
122 124 :type apiuser: AuthUser
123 125 :param userid: Sets the userid for which associated IP Address data
124 126 is returned.
125 127 :type userid: Optional(str or int)
126 128
127 129 Example output:
128 130
129 131 .. code-block:: bash
130 132
131 133 id : <id_given_in_input>
132 134 result : {
133 135 "server_ip_addr": "<ip_from_clien>",
134 136 "user_ips": [
135 137 {
136 138 "ip_addr": "<ip_with_mask>",
137 139 "ip_range": ["<start_ip>", "<end_ip>"],
138 140 },
139 141 ...
140 142 ]
141 143 }
142 144
143 145 """
144 146 if not has_superadmin_permission(apiuser):
145 147 raise JSONRPCForbidden()
146 148
147 149 userid = Optional.extract(userid, evaluate_locals=locals())
148 150 userid = getattr(userid, 'user_id', userid)
149 151
150 152 user = get_user_or_error(userid)
151 153 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
152 154 return {
153 155 'server_ip_addr': request.rpc_ip_addr,
154 156 'user_ips': ips
155 157 }
156 158
157 159
158 160 @jsonrpc_method()
159 161 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
160 162 """
161 163 Triggers a rescan of the specified repositories.
162 164
163 165 * If the ``remove_obsolete`` option is set, it also deletes repositories
164 166 that are found in the database but not on the file system, so called
165 167 "clean zombies".
166 168
167 169 This command can only be run using an |authtoken| with admin rights to
168 170 the specified repository.
169 171
170 172 This command takes the following options:
171 173
172 174 :param apiuser: This is filled automatically from the |authtoken|.
173 175 :type apiuser: AuthUser
174 176 :param remove_obsolete: Deletes repositories from the database that
175 177 are not found on the filesystem.
176 178 :type remove_obsolete: Optional(``True`` | ``False``)
177 179
178 180 Example output:
179 181
180 182 .. code-block:: bash
181 183
182 184 id : <id_given_in_input>
183 185 result : {
184 186 'added': [<added repository name>,...]
185 187 'removed': [<removed repository name>,...]
186 188 }
187 189 error : null
188 190
189 191 Example error output:
190 192
191 193 .. code-block:: bash
192 194
193 195 id : <id_given_in_input>
194 196 result : null
195 197 error : {
196 198 'Error occurred during rescan repositories action'
197 199 }
198 200
199 201 """
200 202 if not has_superadmin_permission(apiuser):
201 203 raise JSONRPCForbidden()
202 204
203 205 try:
204 206 rm_obsolete = Optional.extract(remove_obsolete)
205 207 added, removed = repo2db_mapper(ScmModel().repo_scan(),
206 208 remove_obsolete=rm_obsolete)
207 209 return {'added': added, 'removed': removed}
208 210 except Exception:
209 211 log.exception('Failed to run repo rescann')
210 212 raise JSONRPCError(
211 213 'Error occurred during rescan repositories action'
212 214 )
213 215
214 216
215 217 @jsonrpc_method()
216 218 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
217 219 """
218 220 Triggers a session cleanup action.
219 221
220 222 If the ``older_then`` option is set, only sessions that hasn't been
221 223 accessed in the given number of days will be removed.
222 224
223 225 This command can only be run using an |authtoken| with admin rights to
224 226 the specified repository.
225 227
226 228 This command takes the following options:
227 229
228 230 :param apiuser: This is filled automatically from the |authtoken|.
229 231 :type apiuser: AuthUser
230 232 :param older_then: Deletes session that hasn't been accessed
231 233 in given number of days.
232 234 :type older_then: Optional(int)
233 235
234 236 Example output:
235 237
236 238 .. code-block:: bash
237 239
238 240 id : <id_given_in_input>
239 241 result: {
240 242 "backend": "<type of backend>",
241 243 "sessions_removed": <number_of_removed_sessions>
242 244 }
243 245 error : null
244 246
245 247 Example error output:
246 248
247 249 .. code-block:: bash
248 250
249 251 id : <id_given_in_input>
250 252 result : null
251 253 error : {
252 254 'Error occurred during session cleanup'
253 255 }
254 256
255 257 """
256 258 if not has_superadmin_permission(apiuser):
257 259 raise JSONRPCForbidden()
258 260
259 261 older_then = safe_int(Optional.extract(older_then)) or 60
260 262 older_than_seconds = 60 * 60 * 24 * older_then
261 263
262 264 config = system_info.rhodecode_config().get_value()['value']['config']
263 265 session_model = user_sessions.get_session_handler(
264 266 config.get('beaker.session.type', 'memory'))(config)
265 267
266 268 backend = session_model.SESSION_TYPE
267 269 try:
268 270 cleaned = session_model.clean_sessions(
269 271 older_than_seconds=older_than_seconds)
270 272 return {'sessions_removed': cleaned, 'backend': backend}
271 273 except user_sessions.CleanupCommand as msg:
272 274 return {'cleanup_command': msg.message, 'backend': backend}
273 275 except Exception as e:
274 276 log.exception('Failed session cleanup')
275 277 raise JSONRPCError(
276 278 'Error occurred during session cleanup'
277 279 )
278 280
279 281
280 282 @jsonrpc_method()
281 283 def get_method(request, apiuser, pattern=Optional('*')):
282 284 """
283 285 Returns list of all available API methods. By default match pattern
284 286 os "*" but any other pattern can be specified. eg *comment* will return
285 287 all methods with comment inside them. If just single method is matched
286 288 returned data will also include method specification
287 289
288 290 This command can only be run using an |authtoken| with admin rights to
289 291 the specified repository.
290 292
291 293 This command takes the following options:
292 294
293 295 :param apiuser: This is filled automatically from the |authtoken|.
294 296 :type apiuser: AuthUser
295 297 :param pattern: pattern to match method names against
296 :type older_then: Optional("*")
298 :type pattern: Optional("*")
297 299
298 300 Example output:
299 301
300 302 .. code-block:: bash
301 303
302 304 id : <id_given_in_input>
303 305 "result": [
304 306 "changeset_comment",
305 307 "comment_pull_request",
306 308 "comment_commit"
307 309 ]
308 310 error : null
309 311
310 312 .. code-block:: bash
311 313
312 314 id : <id_given_in_input>
313 315 "result": [
314 316 "comment_commit",
315 317 {
316 318 "apiuser": "<RequiredType>",
317 319 "comment_type": "<Optional:u'note'>",
318 320 "commit_id": "<RequiredType>",
319 321 "message": "<RequiredType>",
320 322 "repoid": "<RequiredType>",
321 323 "request": "<RequiredType>",
322 324 "resolves_comment_id": "<Optional:None>",
323 325 "status": "<Optional:None>",
324 326 "userid": "<Optional:<OptionalAttr:apiuser>>"
325 327 }
326 328 ]
327 329 error : null
328 330 """
329 331 if not has_superadmin_permission(apiuser):
330 332 raise JSONRPCForbidden()
331 333
332 334 pattern = Optional.extract(pattern)
333 335
334 336 matches = find_methods(request.registry.jsonrpc_methods, pattern)
335 337
336 338 args_desc = []
337 339 if len(matches) == 1:
338 340 func = matches[matches.keys()[0]]
339 341
340 342 argspec = inspect.getargspec(func)
341 343 arglist = argspec[0]
342 344 defaults = map(repr, argspec[3] or [])
343 345
344 346 default_empty = '<RequiredType>'
345 347
346 348 # kw arguments required by this method
347 349 func_kwargs = dict(itertools.izip_longest(
348 350 reversed(arglist), reversed(defaults), fillvalue=default_empty))
349 351 args_desc.append(func_kwargs)
350 352
351 353 return matches.keys() + args_desc
354
355
356 @jsonrpc_method()
357 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
358 """
359 Stores sent exception inside the built-in exception tracker in |RCE| server.
360
361 This command can only be run using an |authtoken| with admin rights to
362 the specified repository.
363
364 This command takes the following options:
365
366 :param apiuser: This is filled automatically from the |authtoken|.
367 :type apiuser: AuthUser
368
369 :param exc_data_json: JSON data with exception e.g
370 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
371 :type exc_data_json: JSON data
372
373 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
374 :type prefix: Optional("rhodecode")
375
376 Example output:
377
378 .. code-block:: bash
379
380 id : <id_given_in_input>
381 "result": {
382 "exc_id": 139718459226384,
383 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
384 }
385 error : null
386 """
387 if not has_superadmin_permission(apiuser):
388 raise JSONRPCForbidden()
389
390 prefix = Optional.extract(prefix)
391 exc_id = exc_tracking.generate_id()
392
393 try:
394 exc_data = json.loads(exc_data_json)
395 except Exception:
396 log.error('Failed to parse JSON: %r', exc_data_json)
397 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
398 'Please make sure it contains a valid JSON.')
399
400 try:
401 exc_traceback = exc_data['exc_traceback']
402 exc_type_name = exc_data['exc_type_name']
403 except KeyError as err:
404 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
405 'in exc_data_json field. Missing: {}'.format(err))
406
407 exc_tracking._store_exception(
408 exc_id=exc_id, exc_traceback=exc_traceback,
409 exc_type_name=exc_type_name, prefix=prefix)
410
411 exc_url = request.route_url(
412 'admin_settings_exception_tracker_show', exception_id=exc_id)
413 return {'exc_id': exc_id, 'exc_url': exc_url}
414
@@ -1,151 +1,166 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import time
23 23 import datetime
24 24 import msgpack
25 25 import logging
26 26 import traceback
27 27 import tempfile
28 28
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
33 33 global_prefix = 'rhodecode'
34 34 exc_store_dir_name = 'rc_exception_store_v1'
35 35
36 36
37 37 def exc_serialize(exc_id, tb, exc_type):
38 38
39 39 data = {
40 40 'version': 'v1',
41 41 'exc_id': exc_id,
42 42 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
43 43 'exc_timestamp': repr(time.time()),
44 44 'exc_message': tb,
45 45 'exc_type': exc_type,
46 46 }
47 47 return msgpack.packb(data), data
48 48
49 49
50 50 def exc_unserialize(tb):
51 51 return msgpack.unpackb(tb)
52 52
53 53
54 54 def get_exc_store():
55 55 """
56 56 Get and create exception store if it's not existing
57 57 """
58 58 import rhodecode as app
59 59
60 60 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
61 61 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
62 62
63 63 _exc_store_path = os.path.abspath(_exc_store_path)
64 64 if not os.path.isdir(_exc_store_path):
65 65 os.makedirs(_exc_store_path)
66 66 log.debug('Initializing exceptions store at %s', _exc_store_path)
67 67 return _exc_store_path
68 68
69 69
70 def _store_exception(exc_id, exc_info, prefix):
71 exc_type, exc_value, exc_traceback = exc_info
72 tb = ''.join(traceback.format_exception(
73 exc_type, exc_value, exc_traceback, None))
70 def _store_exception(exc_id, exc_type_name, exc_traceback, prefix):
71 """
72 Low level function to store exception in the exception tracker
73 """
74 74
75 exc_type_name = exc_type.__name__
76 75 exc_store_path = get_exc_store()
77 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
76 exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name)
78 77 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
79 78 if not os.path.isdir(exc_store_path):
80 79 os.makedirs(exc_store_path)
81 80 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
82 81 with open(stored_exc_path, 'wb') as f:
83 82 f.write(exc_data)
84 83 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
85 84
86 85
86 def _prepare_exception(exc_info):
87 exc_type, exc_value, exc_traceback = exc_info
88 exc_type_name = exc_type.__name__
89
90 tb = ''.join(traceback.format_exception(
91 exc_type, exc_value, exc_traceback, None))
92
93 return exc_type_name, tb
94
95
87 96 def store_exception(exc_id, exc_info, prefix=global_prefix):
88 97 """
89 98 Example usage::
90 99
91 100 exc_info = sys.exc_info()
92 101 store_exception(id(exc_info), exc_info)
93 102 """
94 103
95 104 try:
96 _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix)
105 exc_type_name, exc_traceback = _prepare_exception(exc_info)
106 _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
107 exc_traceback=exc_traceback, prefix=prefix)
97 108 except Exception:
98 109 log.exception('Failed to store exception `%s` information', exc_id)
99 110 # there's no way this can fail, it will crash server badly if it does.
100 111 pass
101 112
102 113
103 114 def _find_exc_file(exc_id, prefix=global_prefix):
104 115 exc_store_path = get_exc_store()
105 116 if prefix:
106 117 exc_id = '{}_{}'.format(exc_id, prefix)
107 118 else:
108 119 # search without a prefix
109 120 exc_id = '{}'.format(exc_id)
110 121
111 122 # we need to search the store for such start pattern as above
112 123 for fname in os.listdir(exc_store_path):
113 124 if fname.startswith(exc_id):
114 125 exc_id = os.path.join(exc_store_path, fname)
115 126 break
116 127 continue
117 128 else:
118 129 exc_id = None
119 130
120 131 return exc_id
121 132
122 133
123 134 def _read_exception(exc_id, prefix):
124 135 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
125 136 if exc_id_file_path:
126 137 with open(exc_id_file_path, 'rb') as f:
127 138 return exc_unserialize(f.read())
128 139 else:
129 140 log.debug('Exception File `%s` not found', exc_id_file_path)
130 141 return None
131 142
132 143
133 144 def read_exception(exc_id, prefix=global_prefix):
134 145 try:
135 146 return _read_exception(exc_id=exc_id, prefix=prefix)
136 147 except Exception:
137 148 log.exception('Failed to read exception `%s` information', exc_id)
138 149 # there's no way this can fail, it will crash server badly if it does.
139 150 return None
140 151
141 152
142 153 def delete_exception(exc_id, prefix=global_prefix):
143 154 try:
144 155 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
145 156 if exc_id_file_path:
146 157 os.remove(exc_id_file_path)
147 158
148 159 except Exception:
149 160 log.exception('Failed to remove exception `%s` information', exc_id)
150 161 # there's no way this can fail, it will crash server badly if it does.
151 162 pass
163
164
165 def generate_id():
166 return id(object())
General Comments 0
You need to be logged in to leave comments. Login now