Blame pagure-webhook/pagure-webhook-server.py

Pierre-Yves Chibon 12f5e6
#!/usr/bin/env python
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
"""
Pierre-Yves Chibon 12f5e6
 (c) 2015 - Copyright Red Hat Inc
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
 Authors:
Pierre-Yves Chibon 12f5e6
   Pierre-Yves Chibon <pingou@pingoured.fr></pingou@pingoured.fr>
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
This server listens to message sent via redis and send the corresponding
Pierre-Yves Chibon 12f5e6
web-hook request.
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
Using this mechanism, we no longer block the main application if the
Pierre-Yves Chibon 12f5e6
receiving end is offline or so.
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
"""
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
import datetime
Pierre-Yves Chibon 12f5e6
import hashlib
Pierre-Yves Chibon 12f5e6
import hmac
Pierre-Yves Chibon 12f5e6
import json
Pierre-Yves Chibon 12f5e6
import logging
Pierre-Yves Chibon 12f5e6
import os
Pierre-Yves Chibon 12f5e6
import requests
Pierre-Yves Chibon 12f5e6
import time
Pierre-Yves Chibon 12f5e6
import uuid
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
import six
Pierre-Yves Chibon 12f5e6
import trollius
Pierre-Yves Chibon 12f5e6
import trollius_redis
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
from kitchen.text.converters import to_bytes
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
log = logging.getLogger(__name__)
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
if 'PAGURE_CONFIG' not in os.environ \
Pierre-Yves Chibon 12f5e6
        and os.path.exists('/etc/pagure/pagure.cfg'):
Pierre-Yves Chibon 12f5e6
    print 'Using configuration file `/etc/pagure/pagure.cfg`'
Pierre-Yves Chibon 12f5e6
    os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
import pagure
Pierre-Yves Chibon 12f5e6
import pagure.lib
Pierre-Yves Chibon 12f5e6
from pagure.exceptions import PagureEvException
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
_i = 0
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon d38a61
def call_web_hooks(project, topic, msg, urls):
Pierre-Yves Chibon 12f5e6
    ''' Sends the web-hook notification. '''
Pierre-Yves Chibon fc2b26
    log.info(
Pierre-Yves Chibon fc2b26
        "Processing project: %s - topic: %s", project.fullname, topic)
Pierre-Yves Chibon fc2b26
    log.debug('msg: %s', msg)
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
    # Send web-hooks notification
Pierre-Yves Chibon 12f5e6
    global _i
Pierre-Yves Chibon 12f5e6
    _i += 1
Pierre-Yves Chibon 12f5e6
    year = datetime.datetime.now().year
Pierre-Yves Chibon 12f5e6
    if isinstance(topic, six.text_type):
Pierre-Yves Chibon 12f5e6
        topic = to_bytes(topic, encoding='utf8', nonstring="passthru")
Pierre-Yves Chibon d28610
    msg['pagure_instance'] = pagure.APP.config['APP_URL']
Pierre-Yves Chibon d28610
    msg['project_fullname'] = project.fullname
Pierre-Yves Chibon 12f5e6
    msg = dict(
Pierre-Yves Chibon 12f5e6
        topic=topic.decode('utf-8'),
Pierre-Yves Chibon 12f5e6
        msg=msg,
Pierre-Yves Chibon 12f5e6
        timestamp=int(time.time()),
Pierre-Yves Chibon 12f5e6
        msg_id=str(year) + '-' + str(uuid.uuid4()),
Pierre-Yves Chibon 12f5e6
        i=_i,
Pierre-Yves Chibon 12f5e6
    )
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
    content = json.dumps(msg)
Pierre-Yves Chibon 12f5e6
    hashhex = hmac.new(
Pierre-Yves Chibon 12f5e6
        str(project.hook_token), content, hashlib.sha1).hexdigest()
Patrick Uiterwijk e99b9a
    hashhex256 = hmac.new(
Patrick Uiterwijk e99b9a
        str(project.hook_token), content, hashlib.sha256).hexdigest()
Pierre-Yves Chibon 12f5e6
    headers = {
Pierre-Yves Chibon d28610
        'X-Pagure': pagure.APP.config['APP_URL'],
Pierre-Yves Chibon d28610
        'X-Pagure-project': project.fullname,
Patrick Uiterwijk e99b9a
        'X-Pagure-Signature': hashhex,
Pierre-Yves Chibon d28610
        'X-Pagure-Signature-256': hashhex256,
Pierre-Yves Chibon d28610
        'X-Pagure-Topic': topic,
Pierre-Yves Chibon 39cc17
        'Content-Type': 'application/json',
Pierre-Yves Chibon 12f5e6
    }
Pierre-Yves Chibon d38a61
    for url in urls:
Pierre-Yves Chibon 12f5e6
        url = url.strip()
Pierre-Yves Chibon 12f5e6
        log.info('Calling url %s' % url)
Pierre-Yves Chibon 12f5e6
        try:
Pierre-Yves Chibon 12f5e6
            req = requests.post(
Pierre-Yves Chibon 12f5e6
                url,
Pierre-Yves Chibon 12f5e6
                headers=headers,
Pierre-Yves Chibon 39cc17
                data=content,
Pierre-Yves Chibon d38a61
                timeout=60,
Pierre-Yves Chibon 12f5e6
            )
Pierre-Yves Chibon 12f5e6
            if not req:
Pierre-Yves Chibon 9bd1d8
                log.info(
Pierre-Yves Chibon 12f5e6
                    'An error occured while querying: %s - '
Pierre-Yves Chibon 12f5e6
                    'Error code: %s' % (url, req.status_code))
Pierre-Yves Chibon 12f5e6
        except (requests.exceptions.RequestException, Exception) as err:
Pierre-Yves Chibon 9bd1d8
            log.info(
Pierre-Yves Chibon 12f5e6
                'An error occured while querying: %s - Error: %s' % (
Pierre-Yves Chibon 12f5e6
                    url, err))
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
@trollius.coroutine
Pierre-Yves Chibon af4c20
def handle_messages():
Patrick Uiterwijk b84a96
    host = pagure.APP.config.get('REDIS_HOST', '0.0.0.0')
Patrick Uiterwijk b84a96
    port = pagure.APP.config.get('REDIS_PORT', 6379)
Patrick Uiterwijk b84a96
    dbname = pagure.APP.config.get('REDIS_DB', 0)
Patrick Uiterwijk b84a96
    connection = yield trollius.From(trollius_redis.Connection.create(
Patrick Uiterwijk b84a96
        host=host, port=port, db=dbname))
Patrick Uiterwijk b84a96
Patrick Uiterwijk b84a96
    # Create subscriber.
Patrick Uiterwijk b84a96
    subscriber = yield trollius.From(connection.start_subscribe())
Patrick Uiterwijk b84a96
Patrick Uiterwijk b84a96
    # Subscribe to channel.
Patrick Uiterwijk b84a96
    yield trollius.From(subscriber.subscribe(['pagure.hook']))
Patrick Uiterwijk b84a96
Patrick Uiterwijk b84a96
    # Inside a while loop, wait for incoming events.
Patrick Uiterwijk b84a96
    while True:
Patrick Uiterwijk b84a96
        reply = yield trollius.From(subscriber.next_published())
Patrick Uiterwijk b84a96
        log.info(
Patrick Uiterwijk b84a96
            'Received: %s on channel: %s',
Patrick Uiterwijk b84a96
            repr(reply.value), reply.channel)
Patrick Uiterwijk b84a96
        data = json.loads(reply.value)
Patrick Uiterwijk b84a96
        username = None
Patrick Uiterwijk b84a96
        if data['project'].startswith('forks'):
Patrick Uiterwijk b84a96
            username, projectname = data['project'].split('/', 2)[1:]
Patrick Uiterwijk b84a96
        else:
Patrick Uiterwijk b84a96
            projectname = data['project']
Patrick Uiterwijk b84a96
Patrick Uiterwijk b84a96
        namespace = None
Patrick Uiterwijk b84a96
        if '/' in projectname:
Patrick Uiterwijk b84a96
            namespace, projectname = projectname.split('/', 1)
Patrick Uiterwijk b84a96
Patrick Uiterwijk b84a96
        log.info(
Patrick Uiterwijk b84a96
            'Searching %s/%s/%s' % (username, namespace, projectname))
Patrick Uiterwijk b84a96
        session = pagure.lib.create_session(pagure.APP.config['DB_URL'])
Patrick Uiterwijk b84a96
        project = pagure.lib._get_project(
Patrick Uiterwijk b84a96
            session=session, name=projectname, user=username,
Pierre-Yves Chibon edbdc9
            namespace=namespace,
Pierre-Yves Chibon edbdc9
            case=pagure.APP.config.get('CASE_SENSITIVE', False))
Patrick Uiterwijk b84a96
        if not project:
Patrick Uiterwijk b84a96
            log.info('No project found with these criteria')
Pierre-Yves Chibon d1cbf9
            session.close()
Patrick Uiterwijk b84a96
            continue
Patrick Uiterwijk b84a96
        urls = project.settings.get('Web-hooks')
Patrick Uiterwijk b84a96
        session.close()
Patrick Uiterwijk b84a96
        if not urls:
Patrick Uiterwijk b84a96
            log.info('No URLs set: %s' % urls)
Patrick Uiterwijk b84a96
            continue
Patrick Uiterwijk b84a96
        urls = urls.split('\n')
Patrick Uiterwijk b84a96
        log.info('Got the project, going to the webhooks')
Patrick Uiterwijk b84a96
        call_web_hooks(project, data['topic'], data['msg'], urls)
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
def main():
Pierre-Yves Chibon 12f5e6
    server = None
Pierre-Yves Chibon 12f5e6
    try:
Pierre-Yves Chibon 12f5e6
        loop = trollius.get_event_loop()
Pierre-Yves Chibon 12f5e6
        tasks = [
Pierre-Yves Chibon af4c20
            trollius.async(handle_messages()),
Pierre-Yves Chibon 12f5e6
        ]
Pierre-Yves Chibon 12f5e6
        loop.run_until_complete(trollius.wait(tasks))
Pierre-Yves Chibon 12f5e6
        loop.run_forever()
Pierre-Yves Chibon 12f5e6
    except KeyboardInterrupt:
Pierre-Yves Chibon 12f5e6
        pass
Pierre-Yves Chibon 12f5e6
    except trollius.ConnectionResetError:
Pierre-Yves Chibon 12f5e6
        pass
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
    log.info("End Connection")
Pierre-Yves Chibon 12f5e6
    loop.close()
Pierre-Yves Chibon 12f5e6
    log.info("End")
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
if __name__ == '__main__':
Pierre-Yves Chibon 12f5e6
    log = logging.getLogger("")
Pierre-Yves Chibon 12f5e6
    formatter = logging.Formatter(
Pierre-Yves Chibon 12f5e6
        "%(asctime)s %(levelname)s [%(module)s:%(lineno)d] %(message)s")
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
    # setup console logging
Pierre-Yves Chibon 12f5e6
    log.setLevel(logging.DEBUG)
Pierre-Yves Chibon 12f5e6
    ch = logging.StreamHandler()
Pierre-Yves Chibon 12f5e6
    ch.setLevel(logging.DEBUG)
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
    aslog = logging.getLogger("asyncio")
Pierre-Yves Chibon 12f5e6
    aslog.setLevel(logging.DEBUG)
Pierre-Yves Chibon 12f5e6
Pierre-Yves Chibon 12f5e6
    ch.setFormatter(formatter)
Pierre-Yves Chibon 12f5e6
    log.addHandler(ch)
Pierre-Yves Chibon 12f5e6
    main()