diff --git a/README.md b/README.md index 0f31be20..4574a3f7 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ Eventually this can be tied into the rechecker tool and launchpad Future Work ------------ -- IRC bot output to #openstack-qa with output - Pull in list of queries from a more flexible source, so a commit isn't needed to update each time - Turn into a server app - Make unit tests robust and not need internet diff --git a/bot.py b/bot.py new file mode 100755 index 00000000..21be6a61 --- /dev/null +++ b/bot.py @@ -0,0 +1,192 @@ +#! /usr/bin/env python + +# The configuration file should look like: +""" +[ircbot] +nick=NICKNAME +pass=PASSWORD +server=irc.freenode.net +port=6667 +server_password=SERVERPASS +channel_config=/path/to/yaml/config + +[gerrit] +user=gerrit2 +""" + +# The yaml channel config should look like: +""" +openstack-qa: + events: + - positive + - negative +""" + + +import ConfigParser +import daemon +import lockfile +import irc.bot +import os +import sys +import threading +import time +import yaml +import logging + +from elasticRecheck import Stream +from elasticRecheck import Classifier + + + +class RecheckWatchBot(irc.bot.SingleServerIRCBot): + def __init__(self, channels, nickname, password, server, port=6667, + server_password=None): + irc.bot.SingleServerIRCBot.__init__( + self, [(server, port, server_password)], nickname, nickname) + self.channel_list = channels + self.nickname = nickname + self.password = password + self.log = logging.getLogger('gerritbot') + + def on_nicknameinuse(self, c, e): + self.log.info('Nick previously in use, recovering.') + 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) + self.log.info('Nick previously in use, recovered.') + + def on_welcome(self, c, e): + self.log.info('Identifying with IRC server.') + c.privmsg("nickserv", "identify %s " % self.password) + self.log.info('Identified with IRC server.') + for channel in self.channel_list: + c.join(channel) + self.log.info('Joined channel %s' % channel) + time.sleep(0.5) + + def send(self, channel, msg): + self.log.info('Sending "%s" to %s' % (msg, channel)) + self.connection.privmsg(channel, msg) + time.sleep(0.5) + +class RecheckWatch(threading.Thread): + def __init__(self, ircbot, channel_config, username): + threading.Thread.__init__(self) + self.ircbot = ircbot + self.channel_config = channel_config + self.log = logging.getLogger('recheckwatchbot') + self.username = username + self.connected = False + + def new_error(self, channel, data): + msg = '%s change: %s failed tempest with an unrecognized error' % ( + data['change']['project'], + data['change']['url']) + self.log.info('Compiled Message %s: %s' % (channel, msg)) + self.ircbot.send(channel, msg) + + def error_found(self, channel, data): + msg = ('%s change: %s failed tempest because of: ' + 'https://bugs.launchpad.net/bugs/%s' % ( + data['change']['project'], + data['change']['url'], + data['bug_number'])) + self.log.info('Compiled Message %s: %s' % (channel, msg)) + self.ircbot.send(channel, msg) + + def _read(self, data): + for channel in self.channel_config.channels: + if data.get('bug_number'): + if channel in self.channel_config.events['positive']: + self.error_found(channel, data) + else: + if channel in self.channel_config.events['negative']: + self.new_error(channel, data) + + def run(self): + classifier = Classifier() + stream = Stream(self.username) + while True: + event = stream.get_failed_tempest() + change = event['change']['number'] + rev = event['patchSet']['number'] + bug_number = classifier.classify(change, rev, event['comment']) + if bug_number is None: + self._read(event) + else: + event['bug_number'] = bug_number + self._read(event) + + + +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.events = {} + 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 + + +def _main(): + config = ConfigParser.ConfigParser({'server_password': None}) + config.read(sys.argv[1]) + setup_logging(config) + + 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 = RecheckWatchBot(channel_config.channels, + config.get('ircbot', 'nick'), + config.get('ircbot', 'pass'), + config.get('ircbot', 'server'), + config.getint('ircbot', 'port'), + config.get('ircbot', 'server_password')) + recheck = RecheckWatch(bot, channel_config, config.get('gerrit', 'user')) + + recheck.start() + bot.start() + + +def main(): + if len(sys.argv) != 2: + print "Usage: %s CONFIGFILE" % sys.argv[0] + sys.exit(1) + + pid = lockfile.FileLock( + "/var/run/recheckwatchbot/recheckwatchbot.pid", 10) +# with daemon.DaemonContext(pidfile=pid): + _main() + + +def setup_logging(config): + if config.has_option('ircbot', 'log_config'): + log_config = config.get('ircbot', 'log_config') + fp = os.path.expanduser(log_config) + if not os.path.exists(fp): + raise Exception("Unable to read logging config file at %s" % fp) + logging.config.fileConfig(fp) + else: + logging.basicConfig(level=logging.DEBUG) + + +if __name__ == "__main__": + main() diff --git a/elasticRecheck.conf b/elasticRecheck.conf index e38b7a4a..3e5d800e 100644 --- a/elasticRecheck.conf +++ b/elasticRecheck.conf @@ -1,2 +1,9 @@ +[ircbot] +nick=RecheckWatchBot +pass= +server=irc.freenode.net +port=6667 +channel_config=recheckwatchbot.yaml + [gerrit] user=jogo diff --git a/elasticRecheck.py b/elasticRecheck.py index aec09009..7e7ee50a 100755 --- a/elasticRecheck.py +++ b/elasticRecheck.py @@ -17,11 +17,8 @@ class Stream(object): Monitors gerrit stream looking for tempest-devstack failures. """ - def __init__(self): - config = ConfigParser.ConfigParser() - config.read('elasticRecheck.conf') + def __init__(self, user): host = 'review.openstack.org' - user = config.get('gerrit', 'user', 'jogo') port = 29418 self.gerrit = gerritlib.gerrit.Gerrit(host, user, port) self.gerrit.startWatching() @@ -154,7 +151,9 @@ class Classifier(): def main(): classifier = Classifier() #classifier.test() - stream = Stream() + config = ConfigParser.ConfigParser() + user = config.get('gerrit', 'user', 'jogo') + stream = Stream(user) while True: event = stream.get_failed_tempest() change = event['change']['number'] diff --git a/recheckwatchbot.yaml b/recheckwatchbot.yaml new file mode 100644 index 00000000..d8721501 --- /dev/null +++ b/recheckwatchbot.yaml @@ -0,0 +1,5 @@ +openstack-recheck-watch-test: + events: + - positive + - negative + diff --git a/requirements.txt b/requirements.txt index 0dec9adf..d51d5ea4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ pyelasticsearch gerritlib testtools +daemon +irc +pyyaml +lockfile