##// END OF EJS Templates
vcs: Only allow 'pull' actions on shadow repositories....
vcs: Only allow 'pull' actions on shadow repositories. We are exposing the shadow repositories of pull requests to allow easy CI integration or users to access the pull request shadow repo for investigating on it. But we don't want someone/something to push changes to a shadow repository.

File last commit:

r670:8607f5e9 stable
r891:910a0be0 default
Show More
hooks_daemon.py
279 lines | 8.3 KiB | text/x-python | PythonLexer
project: added all source files and assets
r1 # -*- coding: utf-8 -*-
# Copyright (C) 2010-2016 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import json
import logging
dan
integrations: add integration support...
r411 import urlparse
project: added all source files and assets
r1 import threading
from BaseHTTPServer import BaseHTTPRequestHandler
from SocketServer import TCPServer
dan
integrations: add integration support...
r411 from routes.util import URLGenerator
project: added all source files and assets
r1
import Pyro4
dan
integrations: add integration support...
r411 import pylons
import rhodecode
project: added all source files and assets
r1
dan
db: move Session.remove to outer wsgi layer and also add it...
r669 from rhodecode.model import meta
project: added all source files and assets
r1 from rhodecode.lib import hooks_base
dan
slack: fix links in commit/pr events
r419 from rhodecode.lib.utils2 import (
AttributeDict, safe_str, get_routes_generator_for_server_url)
project: added all source files and assets
r1
log = logging.getLogger(__name__)
class HooksHttpHandler(BaseHTTPRequestHandler):
def do_POST(self):
method, extras = self._read_request()
try:
result = self._call_hook(method, extras)
except Exception as e:
result = {
'exception': e.__class__.__name__,
'exception_args': e.args
}
self._write_response(result)
def _read_request(self):
length = int(self.headers['Content-Length'])
body = self.rfile.read(length).decode('utf-8')
data = json.loads(body)
return data['method'], data['extras']
def _write_response(self, result):
self.send_response(200)
self.send_header("Content-type", "text/json")
self.end_headers()
self.wfile.write(json.dumps(result))
def _call_hook(self, method, extras):
hooks = Hooks()
dan
db: move Session.remove to outer wsgi layer and also add it...
r669 try:
result = getattr(hooks, method)(extras)
finally:
meta.Session.remove()
project: added all source files and assets
r1 return result
def log_message(self, format, *args):
"""
This is an overriden method of BaseHTTPRequestHandler which logs using
logging library instead of writing directly to stderr.
"""
message = format % args
# TODO: mikhail: add different log levels support
log.debug(
"%s - - [%s] %s", self.client_address[0],
self.log_date_time_string(), message)
class DummyHooksCallbackDaemon(object):
def __init__(self):
self.hooks_module = Hooks.__module__
def __enter__(self):
log.debug('Running dummy hooks callback daemon')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
log.debug('Exiting dummy hooks callback daemon')
class ThreadedHookCallbackDaemon(object):
_callback_thread = None
_daemon = None
_done = False
def __init__(self):
self._prepare()
def __enter__(self):
self._run()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._stop()
def _prepare(self):
raise NotImplementedError()
def _run(self):
raise NotImplementedError()
def _stop(self):
raise NotImplementedError()
class Pyro4HooksCallbackDaemon(ThreadedHookCallbackDaemon):
"""
Context manager which will run a callback daemon in a background thread.
"""
hooks_uri = None
def _prepare(self):
log.debug("Preparing callback daemon and registering hook object")
self._daemon = Pyro4.Daemon()
hooks_interface = Hooks()
self.hooks_uri = str(self._daemon.register(hooks_interface))
log.debug("Hooks uri is: %s", self.hooks_uri)
def _run(self):
log.debug("Running event loop of callback daemon in background thread")
callback_thread = threading.Thread(
target=self._daemon.requestLoop,
kwargs={'loopCondition': lambda: not self._done})
callback_thread.daemon = True
callback_thread.start()
self._callback_thread = callback_thread
def _stop(self):
log.debug("Waiting for background thread to finish.")
self._done = True
self._callback_thread.join()
self._daemon.close()
self._daemon = None
self._callback_thread = None
class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
"""
Context manager which will run a callback daemon in a background thread.
"""
hooks_uri = None
IP_ADDRESS = '127.0.0.1'
# From Python docs: Polling reduces our responsiveness to a shutdown
# request and wastes cpu at all other times.
POLL_INTERVAL = 0.1
def _prepare(self):
log.debug("Preparing callback daemon and registering hook object")
self._done = False
self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
_, port = self._daemon.server_address
self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
log.debug("Hooks uri is: %s", self.hooks_uri)
def _run(self):
log.debug("Running event loop of callback daemon in background thread")
callback_thread = threading.Thread(
target=self._daemon.serve_forever,
kwargs={'poll_interval': self.POLL_INTERVAL})
callback_thread.daemon = True
callback_thread.start()
self._callback_thread = callback_thread
def _stop(self):
log.debug("Waiting for background thread to finish.")
self._daemon.shutdown()
self._callback_thread.join()
self._daemon = None
self._callback_thread = None
def prepare_callback_daemon(extras, protocol=None, use_direct_calls=False):
callback_daemon = None
protocol = protocol.lower() if protocol else None
if use_direct_calls:
callback_daemon = DummyHooksCallbackDaemon()
extras['hooks_module'] = callback_daemon.hooks_module
else:
Martin Bornhold
vcs: Raise an error in case of unsupported protocol.
r589 if protocol == 'pyro4':
callback_daemon = Pyro4HooksCallbackDaemon()
elif protocol == 'http':
callback_daemon = HttpHooksCallbackDaemon()
else:
log.error('Unsupported callback daemon protocol "%s"', protocol)
raise Exception('Unsupported callback daemon protocol.')
project: added all source files and assets
r1 extras['hooks_uri'] = callback_daemon.hooks_uri
extras['hooks_protocol'] = protocol
return callback_daemon, extras
class Hooks(object):
"""
Exposes the hooks for remote call backs
"""
@Pyro4.callback
def repo_size(self, extras):
log.debug("Called repo_size of Hooks object")
return self._call_hook(hooks_base.repo_size, extras)
@Pyro4.callback
def pre_pull(self, extras):
log.debug("Called pre_pull of Hooks object")
return self._call_hook(hooks_base.pre_pull, extras)
@Pyro4.callback
def post_pull(self, extras):
log.debug("Called post_pull of Hooks object")
return self._call_hook(hooks_base.post_pull, extras)
@Pyro4.callback
def pre_push(self, extras):
log.debug("Called pre_push of Hooks object")
return self._call_hook(hooks_base.pre_push, extras)
@Pyro4.callback
def post_push(self, extras):
log.debug("Called post_push of Hooks object")
return self._call_hook(hooks_base.post_push, extras)
def _call_hook(self, hook, extras):
extras = AttributeDict(extras)
dan
slack: fix links in commit/pr events
r419 pylons_router = get_routes_generator_for_server_url(extras.server_url)
dan
integrations: add integration support...
r411 pylons.url._push_object(pylons_router)
project: added all source files and assets
r1
try:
result = hook(extras)
except Exception as error:
log.exception('Exception when handling hook %s', hook)
error_args = error.args
return {
'status': 128,
'output': '',
'exception': type(error).__name__,
'exception_args': error_args,
}
dan
integrations: add integration support...
r411 finally:
pylons.url._pop_object()
dan
pyro: remove db Session when callback finishes to avoid leaving...
r670 meta.Session.remove()
dan
integrations: add integration support...
r411
project: added all source files and assets
r1 return {
'status': result.status,
'output': result.output,
}
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass