gerritbot/gerritbot/bot.py

288 lines
9.4 KiB
Python
Executable File

#! /usr/bin/env python
# The configuration file should look like:
"""
[ircbot]
nick=NICKNAME
pass=PASSWORD
server=irc.freenode.net
port=6667
channel_config=/path/to/yaml/config
[gerrit]
user=gerrit2
key=/path/to/id_rsa
host=review.example.com
port=29418
"""
# The yaml channel config should look like:
"""
openstack-dev:
events:
- patchset-created
- change-merged
projects:
- openstack/nova
- openstack/swift
branches:
- master
"""
import irc.bot
import time
import subprocess
import threading
import select
import json
import sys
import os
import ConfigParser
import daemon
import traceback
import yaml
try:
import daemon.pidlockfile
pid_file_module = daemon.pidlockfile
except:
# as of python-daemon 1.6 it doesn't bundle pidlockfile anymore
# instead it depends on lockfile-0.9.1
import daemon.pidfile
pid_file_module = daemon.pidfile
class GerritBot(irc.bot.SingleServerIRCBot):
def __init__(self, channels, nickname, password, server, port=6667):
irc.bot.SingleServerIRCBot.__init__(self,
[(server, port)],
nickname, nickname)
self.channel_list = channels
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)
for channel in self.channel_list:
c.join(channel)
def send(self, channel, msg):
self.connection.privmsg(channel, msg)
time.sleep(0.5)
class Gerrit(threading.Thread):
def __init__(self, ircbot, channel_config,
username, keyfile, server, port=29418):
threading.Thread.__init__(self)
self.ircbot = ircbot
self.channel_config = channel_config
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, channel, data):
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(channel, msg)
def comment_added(self, channel, data):
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(channel, msg)
for approval in data.get('approvals', []):
if (approval['type'] == 'VRIF' and approval['value'] == '-2' and
channel in self.channel_config.events.get(
'x-vrif-minus-2', set())):
msg = 'Verification of a change to %s failed: %s %s' % (
data['change']['project'],
data['change']['subject'],
data['change']['url'])
self.ircbot.send(channel, msg)
if (approval['type'] == 'VRIF' and approval['value'] == '2' and
channel in self.channel_config.events.get(
'x-vrif-plus-2', set())):
msg = 'Verification of a change to %s succeeded: %s %s' % (
data['change']['project'],
data['change']['subject'],
data['change']['url'])
self.ircbot.send(channel, msg)
if (approval['type'] == 'CRVW' and approval['value'] == '-2' and
channel in self.channel_config.events.get(
'x-crvw-minus-2', set())):
msg = 'A change to %s has been rejected: %s %s' % (
data['change']['project'],
data['change']['subject'],
data['change']['url'])
self.ircbot.send(channel, msg)
if (approval['type'] == 'CRVW' and approval['value'] == '2' and
channel in self.channel_config.events.get(
'x-crvw-plus-2', set())):
msg = 'A change to %s has been approved: %s %s' % (
data['change']['project'],
data['change']['subject'],
data['change']['url'])
self.ircbot.send(channel, msg)
def change_merged(self, channel, data):
msg = 'A change was merged to %s: %s %s' % (
data['change']['project'],
data['change']['subject'],
data['change']['url'])
self.ircbot.send(channel, msg)
def _read(self):
l = self.proc.stdout.readline()
data = json.loads(l)
channel_set = (self.channel_config.projects.get(
data['change']['project'], set()) &
self.channel_config.events.get(
data['type'], set()) &
self.channel_config.branches.get(
data['change']['branch'], set()))
for channel in channel_set:
if data['type'] == 'comment-added':
self.comment_added(channel, data)
elif data['type'] == 'patchset-created':
self.patchset_created(channel, data)
elif data['type'] == 'change-merged':
self.change_merged(channel, 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()
class ChannelConfig(object):
def __init__(self, data):
self.data = data
keys = data.keys()
for key in keys:
if key[0] != '#':
data['#' + key] = data.pop(key)
self.channels = data.keys()
self.projects = {}
self.events = {}
self.branches = {}
for channel, val in self.data.iteritems():
for event in val['events']:
event_set = self.events.get(event, set())
event_set.add(channel)
self.events[event] = event_set
for project in val['projects']:
project_set = self.projects.get(project, set())
project_set.add(channel)
self.projects[project] = project_set
for branch in val['branches']:
branch_set = self.branches.get(branch, set())
branch_set.add(channel)
self.branches[branch] = branch_set
def _main():
config = ConfigParser.ConfigParser()
config.read(sys.argv[1])
fp = config.get('ircbot', 'channel_config')
if fp:
fp = os.path.expanduser(fp)
if not os.path.exists(fp):
raise Exception("Unable to read layout config file at %s" % fp)
else:
raise Exception("Channel Config must be specified in config file.")
channel_config = ChannelConfig(yaml.load(open(fp)))
bot = GerritBot(channel_config.channels,
config.get('ircbot', 'nick'),
config.get('ircbot', 'pass'),
config.get('ircbot', 'server'),
config.getint('ircbot', 'port'))
g = Gerrit(bot,
channel_config,
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 = pid_file_module.TimeoutPIDLockFile(
"/var/run/gerritbot/gerritbot.pid", 10)
with daemon.DaemonContext(pidfile=pid):
_main()
if __name__ == "__main__":
main()