From 39d26a6b605c7b6ab85a4b7fa5271d9a25ee409b Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Tue, 1 May 2012 17:25:16 -0400 Subject: [PATCH] Initial gerritbot code. Change-Id: Ife98fa24e731bcbbe719f583b1788870433b7f10 --- gerritbot | 221 +++++++++++++++++++++++++++++++++++++++++++++++++ gerritbot.init | 149 +++++++++++++++++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100755 gerritbot create mode 100755 gerritbot.init diff --git a/gerritbot b/gerritbot new file mode 100755 index 0000000..bc98f04 --- /dev/null +++ b/gerritbot @@ -0,0 +1,221 @@ +#! /usr/bin/env python + +# The configuration file should look like: +""" +[ircbot] +nick=NICKNAME +pass=PASSWORD +channel=CHANNEL +server=irc.freenode.net +port=6667 + +[gerrit] +user=gerrit2 +key=/path/to/id_rsa +host=review.example.com +port=29418 +events=patchset-created, change-merged +branches=master +""" + +import ircbot +import time +import subprocess +import threading +import select +import json +import sys +import ConfigParser +import daemon, daemon.pidlockfile +import traceback + +class GerritBot(ircbot.SingleServerIRCBot): + def __init__(self, channel, nickname, password, server, port=6667): + if channel[0] != '#': channel = '#'+channel + ircbot.SingleServerIRCBot.__init__(self, + [(server, port)], + nickname, nickname) + self.channel = channel + self.nickname = nickname + self.password = password + + def on_nicknameinuse(self, c, e): + c.nick(c.get_nickname() + "_") + c.privmsg("nickserv", "identify %s " % self.password) + c.privmsg("nickserv", "ghost %s %s" % (self.nickname, self.password)) + c.privmsg("nickserv", "release %s %s" % (self.nickname, self.password)) + time.sleep(1) + c.nick(self.nickname) + + def on_welcome(self, c, e): + c.privmsg("nickserv", "identify %s "% self.password) + c.join(self.channel) + + def send(self, msg): + self.connection.privmsg(self.channel, msg) + time.sleep(0.5) + +class Gerrit(threading.Thread): + def __init__(self, ircbot, events, branches, + username, keyfile, server, port=29418): + threading.Thread.__init__(self) + self.ircbot = ircbot + self.events = events + self.branches = branches + self.username = username + self.keyfile = keyfile + self.server = server + self.port = port + self.proc = None + self.poll = select.poll() + + def _open(self): + self.proc = subprocess.Popen(['/usr/bin/ssh', '-p', str(self.port), + '-i', self.keyfile, + '-l', self.username, self.server, + 'gerrit', 'stream-events'], + bufsize=1, + stdin=None, + stdout=subprocess.PIPE, + stderr=None, + ) + self.poll.register(self.proc.stdout) + + def _close(self): + try: + self.poll.unregister(self.proc.stdout) + except: + pass + try: + self.proc.kill() + except: + pass + self.proc = None + + def patchset_created(self, data): + if 'patchset-created' in self.events: + msg = '%s proposed a change to %s: %s %s' % ( + data['patchSet']['uploader']['name'], + data['change']['project'], + data['change']['subject'], + data['change']['url']) + self.ircbot.send(msg) + + def comment_added(self, data): + if 'comment-added' in self.events: + msg = 'A comment has been added to a proposed change to %s: %s %s' % ( + data['change']['project'], + data['change']['subject'], + data['change']['url']) + self.ircbot.send(msg) + + for approval in data.get('approvals', []): + if (approval['type'] == 'VRIF' and approval['value'] == '-1' and + 'x-vrif-minus-1' in self.events): + msg = 'Verification of a change to %s failed: %s %s' % ( + data['change']['project'], + data['change']['subject'], + data['change']['url']) + self.ircbot.send(msg) + + if (approval['type'] == 'VRIF' and approval['value'] == '1' and + 'x-vrif-plus-1' in self.events): + msg = 'Verification of a change to %s succeeded: %s %s' % ( + data['change']['project'], + data['change']['subject'], + data['change']['url']) + self.ircbot.send(msg) + + if (approval['type'] == 'CRVW' and approval['value'] == '-2' and + 'x-crvw-minus-2' in self.events): + msg = 'A change to %s has been rejected: %s %s' % ( + data['change']['project'], + data['change']['subject'], + data['change']['url']) + self.ircbot.send(msg) + + if (approval['type'] == 'CRVW' and approval['value'] == '2' and + 'x-crvw-plus-2' in self.events): + msg = 'A change to %s has been approved: %s %s' % ( + data['change']['project'], + data['change']['subject'], + data['change']['url']) + self.ircbot.send(msg) + + def change_merged(self, data): + if 'change-merged' in self.events: + msg = 'A change was merged to %s: %s %s' % ( + data['change']['project'], + data['change']['subject'], + data['change']['url']) + self.ircbot.send(msg) + + def _read(self): + l = self.proc.stdout.readline() + data = json.loads(l) + # If branches is specified, ignore notifications for other branches + if self.branches and data['change']['branch'] not in self.branches: + return + if data['type'] == 'comment-added': + self.comment_added(data) + elif data['type'] == 'patchset-created': + self.patchset_created(data) + elif data['type'] == 'change-merged': + self.change_merged(data) + + def _listen(self): + while True: + ret = self.poll.poll() + for (fd, event) in ret: + if fd == self.proc.stdout.fileno(): + if event == select.POLLIN: + self._read() + else: + raise Exception("event on ssh connection") + + def _run(self): + try: + if not self.proc: + self._open() + self._listen() + except: + traceback.print_exc() + self._close() + time.sleep(5) + + def run(self): + time.sleep(5) + while True: + self._run() + +def _main(): + config=ConfigParser.ConfigParser() + config.read(sys.argv[1]) + + bot = GerritBot(config.get('ircbot', 'channel'), + config.get('ircbot', 'nick'), + config.get('ircbot', 'pass'), + config.get('ircbot', 'server'), + config.getint('ircbot', 'port')) + g = Gerrit(bot, + config.get('gerrit', 'events'), + config.get('gerrit', 'branches'), + config.get('gerrit', 'user'), + config.get('gerrit', 'key'), + config.get('gerrit', 'host'), + config.getint('gerrit', 'port')) + g.start() + bot.start() + +def main(): + if len(sys.argv) != 2: + print "Usage: %s CONFIGFILE" % sys.argv[0] + sys.exit(1) + + pid = daemon.pidlockfile.TimeoutPIDLockFile("/var/run/gerritbot/gerritbot.pid", 10) + with daemon.DaemonContext(pidfile=pid): + _main() + + +if __name__ == "__main__": + main() diff --git a/gerritbot.init b/gerritbot.init new file mode 100755 index 0000000..6ed1ccb --- /dev/null +++ b/gerritbot.init @@ -0,0 +1,149 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: gerritbot +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Gerrit IRC Bot +# Description: Announces Gerrit events to IRC +### END INIT INFO + +# Author: James Blair + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="GerritBot" +NAME=gerritbot +DAEMON=/home/gerrit2/$NAME +DAEMON_ARGS="/home/gerrit2/gerritbot.config" +PIDFILE=/var/run/$NAME/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME +USER=gerrit2 + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + + mkdir -p /var/run/$NAME + chown $USER /var/run/$NAME + start-stop-daemon --start --quiet --pidfile $PIDFILE -c $USER --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE -c $USER --exec $DAEMON -- \ + $DAEMON_ARGS \ + || return 2 + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --signal 9 --pidfile $PIDFILE + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + rm -f /var/run/$NAME/* + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: