
Fixes bug #1020987 Update GerritBot with the ability to talk on multiple channels. This way a single GerritBot instance can operate in multiple channels for multiple projects. To make this work this change introduces a new channel configuration file (yaml) for GerritBot that specifies each channel that GerritBot should join and the changes that channel is interested in. The config should look something like: channel-foo: events: - patchset-created - change-merged projects: - test/bar - test/foo branches: - master Change-Id: I8e278f9be5182611981a3d912cc323bd3d386fc5
274 lines
9.2 KiB
Python
Executable File
274 lines
9.2 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 ircbot
|
|
import time
|
|
import subprocess
|
|
import threading
|
|
import select
|
|
import json
|
|
import sys
|
|
import os
|
|
import ConfigParser
|
|
import daemon, daemon.pidlockfile
|
|
import traceback
|
|
import yaml
|
|
|
|
class GerritBot(ircbot.SingleServerIRCBot):
|
|
def __init__(self, channels, nickname, password, server, port=6667):
|
|
ircbot.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 = daemon.pidlockfile.TimeoutPIDLockFile(
|
|
"/var/run/gerritbot/gerritbot.pid", 10)
|
|
with daemon.DaemonContext(pidfile=pid):
|
|
_main()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|