##// END OF EJS Templates
exceptions: show original remote traceback, and more info if debug is enabled.
marcink -
r3369:ba0393a0 default
parent child Browse files
Show More
@@ -1,311 +1,311 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 """
22 22 Client for the VCSServer implemented based on HTTP.
23 23 """
24 24
25 25 import copy
26 26 import logging
27 27 import threading
28 28 import urllib2
29 29 import urlparse
30 30 import uuid
31 31 import traceback
32 32
33 33 import pycurl
34 34 import msgpack
35 35 import requests
36 36 from requests.packages.urllib3.util.retry import Retry
37 37
38 38 import rhodecode
39 39 from rhodecode.lib.system_info import get_cert_path
40 40 from rhodecode.lib.vcs import exceptions, CurlSession
41 41
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 # TODO: mikhail: Keep it in sync with vcsserver's
47 47 # HTTPApplication.ALLOWED_EXCEPTIONS
48 48 EXCEPTIONS_MAP = {
49 49 'KeyError': KeyError,
50 50 'URLError': urllib2.URLError,
51 51 }
52 52
53 53
54 54 class RepoMaker(object):
55 55
56 56 def __init__(self, server_and_port, backend_endpoint, backend_type, session_factory):
57 57 self.url = urlparse.urljoin(
58 58 'http://%s' % server_and_port, backend_endpoint)
59 59 self._session_factory = session_factory
60 60 self.backend_type = backend_type
61 61
62 62 def __call__(self, path, config, with_wire=None):
63 63 log.debug('RepoMaker call on %s', path)
64 64 return RemoteRepo(
65 65 path, config, self.url, self._session_factory(),
66 66 with_wire=with_wire)
67 67
68 68 def __getattr__(self, name):
69 69 def f(*args, **kwargs):
70 70 return self._call(name, *args, **kwargs)
71 71 return f
72 72
73 73 @exceptions.map_vcs_exceptions
74 74 def _call(self, name, *args, **kwargs):
75 75 payload = {
76 76 'id': str(uuid.uuid4()),
77 77 'method': name,
78 78 'backend': self.backend_type,
79 79 'params': {'args': args, 'kwargs': kwargs}
80 80 }
81 81 return _remote_call(
82 82 self.url, payload, EXCEPTIONS_MAP, self._session_factory())
83 83
84 84
85 85 class ServiceConnection(object):
86 86 def __init__(self, server_and_port, backend_endpoint, session_factory):
87 87 self.url = urlparse.urljoin(
88 88 'http://%s' % server_and_port, backend_endpoint)
89 89 self._session_factory = session_factory
90 90
91 91 def __getattr__(self, name):
92 92 def f(*args, **kwargs):
93 93 return self._call(name, *args, **kwargs)
94 94
95 95 return f
96 96
97 97 @exceptions.map_vcs_exceptions
98 98 def _call(self, name, *args, **kwargs):
99 99 payload = {
100 100 'id': str(uuid.uuid4()),
101 101 'method': name,
102 102 'params': {'args': args, 'kwargs': kwargs}
103 103 }
104 104 return _remote_call(
105 105 self.url, payload, EXCEPTIONS_MAP, self._session_factory())
106 106
107 107
108 108 class RemoteRepo(object):
109 109
110 110 def __init__(self, path, config, url, session, with_wire=None):
111 111 self.url = url
112 112 self._session = session
113 113 self._wire = {
114 114 "path": path,
115 115 "config": config,
116 116 "context": self._create_vcs_cache_context(),
117 117 }
118 118 if with_wire:
119 119 self._wire.update(with_wire)
120 120
121 121 # johbo: Trading complexity for performance. Avoiding the call to
122 122 # log.debug brings a few percent gain even if is is not active.
123 123 if log.isEnabledFor(logging.DEBUG):
124 124 self._call = self._call_with_logging
125 125
126 126 self.cert_dir = get_cert_path(rhodecode.CONFIG.get('__file__'))
127 127
128 128 def __getattr__(self, name):
129 129 def f(*args, **kwargs):
130 130 return self._call(name, *args, **kwargs)
131 131 return f
132 132
133 133 @exceptions.map_vcs_exceptions
134 134 def _call(self, name, *args, **kwargs):
135 135 # TODO: oliver: This is currently necessary pre-call since the
136 136 # config object is being changed for hooking scenarios
137 137 wire = copy.deepcopy(self._wire)
138 138 wire["config"] = wire["config"].serialize()
139 139
140 140 wire["config"].append(('vcs', 'ssl_dir', self.cert_dir))
141 141 payload = {
142 142 'id': str(uuid.uuid4()),
143 143 'method': name,
144 144 'params': {'wire': wire, 'args': args, 'kwargs': kwargs}
145 145 }
146 146 return _remote_call(self.url, payload, EXCEPTIONS_MAP, self._session)
147 147
148 148 def _call_with_logging(self, name, *args, **kwargs):
149 149 context_uid = self._wire.get('context')
150 150 log.debug('Calling %s@%s with args:%r. wire_context: %s',
151 151 self.url, name, args, context_uid)
152 152 return RemoteRepo._call(self, name, *args, **kwargs)
153 153
154 154 def __getitem__(self, key):
155 155 return self.revision(key)
156 156
157 157 def _create_vcs_cache_context(self):
158 158 """
159 159 Creates a unique string which is passed to the VCSServer on every
160 160 remote call. It is used as cache key in the VCSServer.
161 161 """
162 162 return str(uuid.uuid4())
163 163
164 164 def invalidate_vcs_cache(self):
165 165 """
166 166 This invalidates the context which is sent to the VCSServer on every
167 167 call to a remote method. It forces the VCSServer to create a fresh
168 168 repository instance on the next call to a remote method.
169 169 """
170 170 self._wire['context'] = self._create_vcs_cache_context()
171 171
172 172
173 173 class RemoteObject(object):
174 174
175 175 def __init__(self, url, session):
176 176 self._url = url
177 177 self._session = session
178 178
179 179 # johbo: Trading complexity for performance. Avoiding the call to
180 180 # log.debug brings a few percent gain even if is is not active.
181 181 if log.isEnabledFor(logging.DEBUG):
182 182 self._call = self._call_with_logging
183 183
184 184 def __getattr__(self, name):
185 185 def f(*args, **kwargs):
186 186 return self._call(name, *args, **kwargs)
187 187 return f
188 188
189 189 @exceptions.map_vcs_exceptions
190 190 def _call(self, name, *args, **kwargs):
191 191 payload = {
192 192 'id': str(uuid.uuid4()),
193 193 'method': name,
194 194 'params': {'args': args, 'kwargs': kwargs}
195 195 }
196 196 return _remote_call(self._url, payload, EXCEPTIONS_MAP, self._session)
197 197
198 198 def _call_with_logging(self, name, *args, **kwargs):
199 199 log.debug('Calling %s@%s', self._url, name)
200 200 return RemoteObject._call(self, name, *args, **kwargs)
201 201
202 202
203 203 def _remote_call(url, payload, exceptions_map, session):
204 204 try:
205 205 response = session.post(url, data=msgpack.packb(payload))
206 206 except pycurl.error as e:
207 207 msg = '{}. \npycurl traceback: {}'.format(e, traceback.format_exc())
208 208 raise exceptions.HttpVCSCommunicationError(msg)
209 209 except Exception as e:
210 210 message = getattr(e, 'message', '')
211 211 if 'Failed to connect' in message:
212 212 # gevent doesn't return proper pycurl errors
213 213 raise exceptions.HttpVCSCommunicationError(e)
214 214 else:
215 215 raise
216 216
217 217 if response.status_code >= 400:
218 218 log.error('Call to %s returned non 200 HTTP code: %s',
219 219 url, response.status_code)
220 220 raise exceptions.HttpVCSCommunicationError(repr(response.content))
221 221
222 222 try:
223 223 response = msgpack.unpackb(response.content)
224 224 except Exception:
225 225 log.exception('Failed to decode response %r', response.content)
226 226 raise
227 227
228 228 error = response.get('error')
229 229 if error:
230 230 type_ = error.get('type', 'Exception')
231 231 exc = exceptions_map.get(type_, Exception)
232 232 exc = exc(error.get('message'))
233 233 try:
234 234 exc._vcs_kind = error['_vcs_kind']
235 235 except KeyError:
236 236 pass
237 237
238 238 try:
239 239 exc._vcs_server_traceback = error['traceback']
240 exc._vcs_server_org_exc_name = error['org_exc']
241 exc._vcs_server_org_exc_tb = error['org_exc_tb']
240 242 except KeyError:
241 243 pass
242 244
243 245 raise exc
244 246 return response.get('result')
245 247
246 248
247 249 class VcsHttpProxy(object):
248 250
249 251 CHUNK_SIZE = 16384
250 252
251 253 def __init__(self, server_and_port, backend_endpoint):
252
253
254 254 retries = Retry(total=5, connect=None, read=None, redirect=None)
255 255
256 256 adapter = requests.adapters.HTTPAdapter(max_retries=retries)
257 257 self.base_url = urlparse.urljoin(
258 258 'http://%s' % server_and_port, backend_endpoint)
259 259 self.session = requests.Session()
260 260 self.session.mount('http://', adapter)
261 261
262 262 def handle(self, environment, input_data, *args, **kwargs):
263 263 data = {
264 264 'environment': environment,
265 265 'input_data': input_data,
266 266 'args': args,
267 267 'kwargs': kwargs
268 268 }
269 269 result = self.session.post(
270 270 self.base_url, msgpack.packb(data), stream=True)
271 271 return self._get_result(result)
272 272
273 273 def _deserialize_and_raise(self, error):
274 274 exception = Exception(error['message'])
275 275 try:
276 276 exception._vcs_kind = error['_vcs_kind']
277 277 except KeyError:
278 278 pass
279 279 raise exception
280 280
281 281 def _iterate(self, result):
282 282 unpacker = msgpack.Unpacker()
283 283 for line in result.iter_content(chunk_size=self.CHUNK_SIZE):
284 284 unpacker.feed(line)
285 285 for chunk in unpacker:
286 286 yield chunk
287 287
288 288 def _get_result(self, result):
289 289 iterator = self._iterate(result)
290 290 error = iterator.next()
291 291 if error:
292 292 self._deserialize_and_raise(error)
293 293
294 294 status = iterator.next()
295 295 headers = iterator.next()
296 296
297 297 return iterator, status, headers
298 298
299 299
300 300 class ThreadlocalSessionFactory(object):
301 301 """
302 302 Creates one CurlSession per thread on demand.
303 303 """
304 304
305 305 def __init__(self):
306 306 self._thread_local = threading.local()
307 307
308 308 def __call__(self):
309 309 if not hasattr(self._thread_local, 'curl_session'):
310 310 self._thread_local.curl_session = CurlSession()
311 311 return self._thread_local.curl_session
@@ -1,215 +1,225 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 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 """
22 22 Custom vcs exceptions module.
23 23 """
24 24 import logging
25 25 import functools
26 26 import urllib2
27 import rhodecode
27 28
28 29 log = logging.getLogger(__name__)
29 30
30 31
31 32 class VCSCommunicationError(Exception):
32 33 pass
33 34
34 35
35 36 class HttpVCSCommunicationError(VCSCommunicationError):
36 37 pass
37 38
38 39
39 40 class VCSError(Exception):
40 41 pass
41 42
42 43
43 44 class RepositoryError(VCSError):
44 45 pass
45 46
46 47
47 48 class RepositoryRequirementError(RepositoryError):
48 49 pass
49 50
50 51
51 52 class VCSBackendNotSupportedError(VCSError):
52 53 """
53 54 Exception raised when VCSServer does not support requested backend
54 55 """
55 56
56 57
57 58 class EmptyRepositoryError(RepositoryError):
58 59 pass
59 60
60 61
61 62 class TagAlreadyExistError(RepositoryError):
62 63 pass
63 64
64 65
65 66 class TagDoesNotExistError(RepositoryError):
66 67 pass
67 68
68 69
69 70 class BranchAlreadyExistError(RepositoryError):
70 71 pass
71 72
72 73
73 74 class BranchDoesNotExistError(RepositoryError):
74 75 pass
75 76
76 77
77 78 class CommitError(RepositoryError):
78 79 """
79 80 Exceptions related to an existing commit
80 81 """
81 82
82 83
83 84 class CommitDoesNotExistError(CommitError):
84 85 pass
85 86
86 87
87 88 class CommittingError(RepositoryError):
88 89 """
89 90 Exceptions happening while creating a new commit
90 91 """
91 92
92 93
93 94 class NothingChangedError(CommittingError):
94 95 pass
95 96
96 97
97 98 class NodeError(VCSError):
98 99 pass
99 100
100 101
101 102 class RemovedFileNodeError(NodeError):
102 103 pass
103 104
104 105
105 106 class NodeAlreadyExistsError(CommittingError):
106 107 pass
107 108
108 109
109 110 class NodeAlreadyChangedError(CommittingError):
110 111 pass
111 112
112 113
113 114 class NodeDoesNotExistError(CommittingError):
114 115 pass
115 116
116 117
117 118 class NodeNotChangedError(CommittingError):
118 119 pass
119 120
120 121
121 122 class NodeAlreadyAddedError(CommittingError):
122 123 pass
123 124
124 125
125 126 class NodeAlreadyRemovedError(CommittingError):
126 127 pass
127 128
128 129
129 130 class SubrepoMergeError(RepositoryError):
130 131 """
131 132 This happens if we try to merge a repository which contains subrepos and
132 133 the subrepos cannot be merged. The subrepos are not merged itself but
133 134 their references in the root repo are merged.
134 135 """
135 136
136 137
137 138 class ImproperArchiveTypeError(VCSError):
138 139 pass
139 140
140 141
141 142 class CommandError(VCSError):
142 143 pass
143 144
144 145
145 146 class UnhandledException(VCSError):
146 147 """
147 148 Signals that something unexpected went wrong.
148 149
149 150 This usually means we have a programming error on the side of the VCSServer
150 151 and should inspect the logfile of the VCSServer to find more details.
151 152 """
152 153
153 154
154 155 _EXCEPTION_MAP = {
155 156 'abort': RepositoryError,
156 157 'archive': ImproperArchiveTypeError,
157 158 'error': RepositoryError,
158 159 'lookup': CommitDoesNotExistError,
159 160 'repo_locked': RepositoryError,
160 161 'requirement': RepositoryRequirementError,
161 162 'unhandled': UnhandledException,
162 163 # TODO: johbo: Define our own exception for this and stop abusing
163 164 # urllib's exception class.
164 165 'url_error': urllib2.URLError,
165 166 'subrepo_merge_error': SubrepoMergeError,
166 167 }
167 168
168 169
169 170 def map_vcs_exceptions(func):
170 171 """
171 172 Utility to decorate functions so that plain exceptions are translated.
172 173
173 174 The translation is based on `exc_map` which maps a `str` indicating
174 175 the error type into an exception class representing this error inside
175 176 of the vcs layer.
176 177 """
177 178
178 179 @functools.wraps(func)
179 180 def wrapper(*args, **kwargs):
180 181 try:
181 182 return func(*args, **kwargs)
182 183 except Exception as e:
184 from rhodecode.lib.utils2 import str2bool
185 debug = str2bool(rhodecode.CONFIG.get('debug'))
186
183 187 # The error middleware adds information if it finds
184 188 # __traceback_info__ in a frame object. This way the remote
185 189 # traceback information is made available in error reports.
186 190 remote_tb = getattr(e, '_vcs_server_traceback', None)
191 org_remote_tb = getattr(e, '_vcs_server_org_exc_tb', '')
187 192 __traceback_info__ = None
188 193 if remote_tb:
189 194 if isinstance(remote_tb, basestring):
190 195 remote_tb = [remote_tb]
191 196 __traceback_info__ = (
192 'Found VCSServer remote traceback information:\n\n' +
193 '\n'.join(remote_tb))
197 'Found VCSServer remote traceback information:\n'
198 '{}\n'
199 '+++ BEG SOURCE EXCEPTION +++\n\n'
200 '{}\n'
201 '+++ END SOURCE EXCEPTION +++\n'
202 ''.format('\n'.join(remote_tb), org_remote_tb)
203 )
194 204
195 205 # Avoid that remote_tb also appears in the frame
196 206 del remote_tb
197 207
198 208 # Special vcs errors had an attribute "_vcs_kind" which is used
199 209 # to translate them to the proper exception class in the vcs
200 210 # client layer.
201 211 kind = getattr(e, '_vcs_kind', None)
202 212
203 213 if kind:
204 214 if any(e.args):
205 215 args = e.args
206 216 else:
207 217 args = [__traceback_info__ or 'unhandledException']
208 if __traceback_info__ and kind not in ['unhandled', 'lookup']:
218 if debug or __traceback_info__ and kind not in ['unhandled', 'lookup']:
209 219 # for other than unhandled errors also log the traceback
210 # can be usefull for debugging
220 # can be useful for debugging
211 221 log.error(__traceback_info__)
212 222 raise _EXCEPTION_MAP[kind](*args)
213 223 else:
214 224 raise
215 225 return wrapper
General Comments 0
You need to be logged in to leave comments. Login now