##// END OF EJS Templates
hooks: inject request.user for proper url generation in async methods....
marcink -
r2418:b8cb7843 default
parent child Browse files
Show More
@@ -1,244 +1,251 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import json
21 import json
22 import logging
22 import logging
23 import traceback
23 import traceback
24 import threading
24 import threading
25 from BaseHTTPServer import BaseHTTPRequestHandler
25 from BaseHTTPServer import BaseHTTPRequestHandler
26 from SocketServer import TCPServer
26 from SocketServer import TCPServer
27
27
28 import rhodecode
28 import rhodecode
29 from rhodecode.model import meta
29 from rhodecode.model import meta
30 from rhodecode.lib.base import bootstrap_request, bootstrap_config
30 from rhodecode.lib.base import bootstrap_request, bootstrap_config
31 from rhodecode.lib import hooks_base
31 from rhodecode.lib import hooks_base
32 from rhodecode.lib.utils2 import AttributeDict
32 from rhodecode.lib.utils2 import AttributeDict
33
33
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class HooksHttpHandler(BaseHTTPRequestHandler):
38 class HooksHttpHandler(BaseHTTPRequestHandler):
39 def do_POST(self):
39 def do_POST(self):
40 method, extras = self._read_request()
40 method, extras = self._read_request()
41 try:
41 try:
42 result = self._call_hook(method, extras)
42 result = self._call_hook(method, extras)
43 except Exception as e:
43 except Exception as e:
44 exc_tb = traceback.format_exc()
44 exc_tb = traceback.format_exc()
45 result = {
45 result = {
46 'exception': e.__class__.__name__,
46 'exception': e.__class__.__name__,
47 'exception_traceback': exc_tb,
47 'exception_traceback': exc_tb,
48 'exception_args': e.args
48 'exception_args': e.args
49 }
49 }
50 self._write_response(result)
50 self._write_response(result)
51
51
52 def _read_request(self):
52 def _read_request(self):
53 length = int(self.headers['Content-Length'])
53 length = int(self.headers['Content-Length'])
54 body = self.rfile.read(length).decode('utf-8')
54 body = self.rfile.read(length).decode('utf-8')
55 data = json.loads(body)
55 data = json.loads(body)
56 return data['method'], data['extras']
56 return data['method'], data['extras']
57
57
58 def _write_response(self, result):
58 def _write_response(self, result):
59 self.send_response(200)
59 self.send_response(200)
60 self.send_header("Content-type", "text/json")
60 self.send_header("Content-type", "text/json")
61 self.end_headers()
61 self.end_headers()
62 self.wfile.write(json.dumps(result))
62 self.wfile.write(json.dumps(result))
63
63
64 def _call_hook(self, method, extras):
64 def _call_hook(self, method, extras):
65 hooks = Hooks()
65 hooks = Hooks()
66 try:
66 try:
67 result = getattr(hooks, method)(extras)
67 result = getattr(hooks, method)(extras)
68 finally:
68 finally:
69 meta.Session.remove()
69 meta.Session.remove()
70 return result
70 return result
71
71
72 def log_message(self, format, *args):
72 def log_message(self, format, *args):
73 """
73 """
74 This is an overridden method of BaseHTTPRequestHandler which logs using
74 This is an overridden method of BaseHTTPRequestHandler which logs using
75 logging library instead of writing directly to stderr.
75 logging library instead of writing directly to stderr.
76 """
76 """
77
77
78 message = format % args
78 message = format % args
79
79
80 # TODO: mikhail: add different log levels support
80 # TODO: mikhail: add different log levels support
81 log.debug(
81 log.debug(
82 "%s - - [%s] %s", self.client_address[0],
82 "%s - - [%s] %s", self.client_address[0],
83 self.log_date_time_string(), message)
83 self.log_date_time_string(), message)
84
84
85
85
86 class DummyHooksCallbackDaemon(object):
86 class DummyHooksCallbackDaemon(object):
87 def __init__(self):
87 def __init__(self):
88 self.hooks_module = Hooks.__module__
88 self.hooks_module = Hooks.__module__
89
89
90 def __enter__(self):
90 def __enter__(self):
91 log.debug('Running dummy hooks callback daemon')
91 log.debug('Running dummy hooks callback daemon')
92 return self
92 return self
93
93
94 def __exit__(self, exc_type, exc_val, exc_tb):
94 def __exit__(self, exc_type, exc_val, exc_tb):
95 log.debug('Exiting dummy hooks callback daemon')
95 log.debug('Exiting dummy hooks callback daemon')
96
96
97
97
98 class ThreadedHookCallbackDaemon(object):
98 class ThreadedHookCallbackDaemon(object):
99
99
100 _callback_thread = None
100 _callback_thread = None
101 _daemon = None
101 _daemon = None
102 _done = False
102 _done = False
103
103
104 def __init__(self):
104 def __init__(self):
105 self._prepare()
105 self._prepare()
106
106
107 def __enter__(self):
107 def __enter__(self):
108 self._run()
108 self._run()
109 return self
109 return self
110
110
111 def __exit__(self, exc_type, exc_val, exc_tb):
111 def __exit__(self, exc_type, exc_val, exc_tb):
112 log.debug('Callback daemon exiting now...')
112 log.debug('Callback daemon exiting now...')
113 self._stop()
113 self._stop()
114
114
115 def _prepare(self):
115 def _prepare(self):
116 raise NotImplementedError()
116 raise NotImplementedError()
117
117
118 def _run(self):
118 def _run(self):
119 raise NotImplementedError()
119 raise NotImplementedError()
120
120
121 def _stop(self):
121 def _stop(self):
122 raise NotImplementedError()
122 raise NotImplementedError()
123
123
124
124
125 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
125 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
126 """
126 """
127 Context manager which will run a callback daemon in a background thread.
127 Context manager which will run a callback daemon in a background thread.
128 """
128 """
129
129
130 hooks_uri = None
130 hooks_uri = None
131
131
132 IP_ADDRESS = '127.0.0.1'
132 IP_ADDRESS = '127.0.0.1'
133
133
134 # From Python docs: Polling reduces our responsiveness to a shutdown
134 # From Python docs: Polling reduces our responsiveness to a shutdown
135 # request and wastes cpu at all other times.
135 # request and wastes cpu at all other times.
136 POLL_INTERVAL = 0.1
136 POLL_INTERVAL = 0.1
137
137
138 def _prepare(self):
138 def _prepare(self):
139 log.debug("Preparing HTTP callback daemon and registering hook object")
139 log.debug("Preparing HTTP callback daemon and registering hook object")
140
140
141 self._done = False
141 self._done = False
142 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
142 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
143 _, port = self._daemon.server_address
143 _, port = self._daemon.server_address
144 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
144 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
145
145
146 log.debug("Hooks uri is: %s", self.hooks_uri)
146 log.debug("Hooks uri is: %s", self.hooks_uri)
147
147
148 def _run(self):
148 def _run(self):
149 log.debug("Running event loop of callback daemon in background thread")
149 log.debug("Running event loop of callback daemon in background thread")
150 callback_thread = threading.Thread(
150 callback_thread = threading.Thread(
151 target=self._daemon.serve_forever,
151 target=self._daemon.serve_forever,
152 kwargs={'poll_interval': self.POLL_INTERVAL})
152 kwargs={'poll_interval': self.POLL_INTERVAL})
153 callback_thread.daemon = True
153 callback_thread.daemon = True
154 callback_thread.start()
154 callback_thread.start()
155 self._callback_thread = callback_thread
155 self._callback_thread = callback_thread
156
156
157 def _stop(self):
157 def _stop(self):
158 log.debug("Waiting for background thread to finish.")
158 log.debug("Waiting for background thread to finish.")
159 self._daemon.shutdown()
159 self._daemon.shutdown()
160 self._callback_thread.join()
160 self._callback_thread.join()
161 self._daemon = None
161 self._daemon = None
162 self._callback_thread = None
162 self._callback_thread = None
163 log.debug("Background thread done.")
163 log.debug("Background thread done.")
164
164
165
165
166 def prepare_callback_daemon(extras, protocol, use_direct_calls):
166 def prepare_callback_daemon(extras, protocol, use_direct_calls):
167 callback_daemon = None
167 callback_daemon = None
168
168
169 if use_direct_calls:
169 if use_direct_calls:
170 callback_daemon = DummyHooksCallbackDaemon()
170 callback_daemon = DummyHooksCallbackDaemon()
171 extras['hooks_module'] = callback_daemon.hooks_module
171 extras['hooks_module'] = callback_daemon.hooks_module
172 else:
172 else:
173 if protocol == 'http':
173 if protocol == 'http':
174 callback_daemon = HttpHooksCallbackDaemon()
174 callback_daemon = HttpHooksCallbackDaemon()
175 else:
175 else:
176 log.error('Unsupported callback daemon protocol "%s"', protocol)
176 log.error('Unsupported callback daemon protocol "%s"', protocol)
177 raise Exception('Unsupported callback daemon protocol.')
177 raise Exception('Unsupported callback daemon protocol.')
178
178
179 extras['hooks_uri'] = callback_daemon.hooks_uri
179 extras['hooks_uri'] = callback_daemon.hooks_uri
180 extras['hooks_protocol'] = protocol
180 extras['hooks_protocol'] = protocol
181
181
182 log.debug('Prepared a callback daemon: %s', callback_daemon)
182 log.debug('Prepared a callback daemon: %s', callback_daemon)
183 return callback_daemon, extras
183 return callback_daemon, extras
184
184
185
185
186 class Hooks(object):
186 class Hooks(object):
187 """
187 """
188 Exposes the hooks for remote call backs
188 Exposes the hooks for remote call backs
189 """
189 """
190
190
191 def repo_size(self, extras):
191 def repo_size(self, extras):
192 log.debug("Called repo_size of %s object", self)
192 log.debug("Called repo_size of %s object", self)
193 return self._call_hook(hooks_base.repo_size, extras)
193 return self._call_hook(hooks_base.repo_size, extras)
194
194
195 def pre_pull(self, extras):
195 def pre_pull(self, extras):
196 log.debug("Called pre_pull of %s object", self)
196 log.debug("Called pre_pull of %s object", self)
197 return self._call_hook(hooks_base.pre_pull, extras)
197 return self._call_hook(hooks_base.pre_pull, extras)
198
198
199 def post_pull(self, extras):
199 def post_pull(self, extras):
200 log.debug("Called post_pull of %s object", self)
200 log.debug("Called post_pull of %s object", self)
201 return self._call_hook(hooks_base.post_pull, extras)
201 return self._call_hook(hooks_base.post_pull, extras)
202
202
203 def pre_push(self, extras):
203 def pre_push(self, extras):
204 log.debug("Called pre_push of %s object", self)
204 log.debug("Called pre_push of %s object", self)
205 return self._call_hook(hooks_base.pre_push, extras)
205 return self._call_hook(hooks_base.pre_push, extras)
206
206
207 def post_push(self, extras):
207 def post_push(self, extras):
208 log.debug("Called post_push of %s object", self)
208 log.debug("Called post_push of %s object", self)
209 return self._call_hook(hooks_base.post_push, extras)
209 return self._call_hook(hooks_base.post_push, extras)
210
210
211 def _call_hook(self, hook, extras):
211 def _call_hook(self, hook, extras):
212 extras = AttributeDict(extras)
212 extras = AttributeDict(extras)
213 server_url = extras['server_url']
213 server_url = extras['server_url']
214 request = bootstrap_request(application_url=server_url)
214 request = bootstrap_request(application_url=server_url)
215
215 bootstrap_config(request) # inject routes and other interfaces
216 bootstrap_config(request) # inject routes and other interfaces
217
218 # inject the user for usage in hooks
219 request.user = AttributeDict({'username': extras.username,
220 'ip_addr': extras.ip,
221 'user_id': extras.user_id})
222
216 extras.request = request
223 extras.request = request
217
224
218 try:
225 try:
219 result = hook(extras)
226 result = hook(extras)
220 except Exception as error:
227 except Exception as error:
221 exc_tb = traceback.format_exc()
228 exc_tb = traceback.format_exc()
222 log.exception('Exception when handling hook %s', hook)
229 log.exception('Exception when handling hook %s', hook)
223 error_args = error.args
230 error_args = error.args
224 return {
231 return {
225 'status': 128,
232 'status': 128,
226 'output': '',
233 'output': '',
227 'exception': type(error).__name__,
234 'exception': type(error).__name__,
228 'exception_traceback': exc_tb,
235 'exception_traceback': exc_tb,
229 'exception_args': error_args,
236 'exception_args': error_args,
230 }
237 }
231 finally:
238 finally:
232 meta.Session.remove()
239 meta.Session.remove()
233
240
234 log.debug('Got hook call response %s', result)
241 log.debug('Got hook call response %s', result)
235 return {
242 return {
236 'status': result.status,
243 'status': result.status,
237 'output': result.output,
244 'output': result.output,
238 }
245 }
239
246
240 def __enter__(self):
247 def __enter__(self):
241 return self
248 return self
242
249
243 def __exit__(self, exc_type, exc_val, exc_tb):
250 def __exit__(self, exc_type, exc_val, exc_tb):
244 pass
251 pass
General Comments 0
You need to be logged in to leave comments. Login now