#! /usr/bin/env python # Copyright 2011, 2013 OpenStack Foundation # Copyright 2012 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # The configuration file should look like: """ [ircbot] nick=NICKNAME pass=PASSWORD server=irc.freenode.net port=6667 channels=foo,bar nicks=alice,bob [wiki] user=StatusBot password=password url=https://wiki.example.com/w/api.php pageid=1781 """ import argparse import ConfigParser import daemon import irc.bot import logging.config import os import threading import time import simplemediawiki import datetime import re 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 UpdateInterface(object): def alert(self, msg=None): raise NotImplementedError() def notice(self, msg=None): raise NotImplementedError() def log(self, msg=None): raise NotImplementedError() def ok(self, msg=None): raise NotImplementedError() class StatusPage(UpdateInterface): alert_re = re.compile(r'{{CI Alert\|(.*?)}}') item_re = re.compile(r'^\* (.*)$') def __init__(self, config): self.url = config.get('wiki', 'url') self.pageid = config.get('wiki', 'pageid') self.username = config.get('wiki', 'username') self.password = config.get('wiki', 'password') self.current_alert = None self.items = [] def alert(self, msg): self.update(set_alert=True, msg=msg) def notice(self, msg): self.update(msg=msg) def log(self, msg): self.update(msg=msg) def ok(self, msg): self.update(clear_alert=True, msg=msg) def update(self, set_alert=None, clear_alert=None, msg=None): self.wiki = simplemediawiki.MediaWiki(self.url) self.wiki.login(self.username, self.password) self.load() if set_alert: self.setAlert(msg) if clear_alert: self.setAlert(None) if msg: self.addItem(msg) self.save() def load(self): self.current_alert = None self.items = [] data = self.wiki.call(dict(action='query', prop='revisions', rvprop='content', pageids=self.pageid, format='json')) text = data['query']['pages'][str(self.pageid)]['revisions'][0]['*'] for line in text.split('\n'): m = self.alert_re.match(line) if m: self.current_alert = m.group(1) m = self.item_re.match(line) if m: self.items.append(m.group(1)) def save(self): text = '' if self.current_alert: text += '{{CI Alert|%s}}\n\n' % self.current_alert for item in self.items: text += '* %s\n' % item data = self.wiki.call(dict(action='query', prop='info', pageids=self.pageid, intoken='edit')) token = data['query']['pages'][str(self.pageid)]['edittoken'] data = self.wiki.call(dict(action='edit', pageid=self.pageid, bot=True, text=text, token=token)) def addItem(self, item, ts=None): if not ts: ts = datetime.datetime.now() text = '%s %s' % (ts.strftime("%Y-%m-%d %H:%M:%S UTC"), item) self.items.insert(0, text) def setAlert(self, current_alert): self.current_alert = current_alert class StatusBot(irc.bot.SingleServerIRCBot): log = logging.getLogger("statusbot.bot") def __init__(self, channels, nicks, publishers, nickname, password, server, port=6667): irc.bot.SingleServerIRCBot.__init__(self, [(server, port)], nickname, nickname) self.channel_list = channels self.nicks = nicks self.nickname = nickname self.password = password self.identify_msg_cap = False self.ignore_topics = True self.topic_lock = threading.Lock() self.topics = {} self.publishers = publishers def on_nicknameinuse(self, c, e): self.log.debug("Nickname in use, releasing") 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): self.identify_msg_cap = False self.log.debug("Requesting identify-msg capability") c.cap('REQ', 'identify-msg') c.cap('END') self.log.debug("Identifying to nickserv") c.privmsg("nickserv", "identify %s " % self.password) for channel in self.channel_list: self.log.info("Joining %s" % channel) c.join(channel) def on_cap(self, c, e): self.log.debug("Received cap response %s" % repr(e.arguments)) if e.arguments[0] == 'ACK' and 'identify-msg' in e.arguments[1]: self.log.debug("identify-msg cap acked") self.identify_msg_cap = True def on_pubmsg(self, c, e): if not self.identify_msg_cap: self.log.debug("Ignoring message because identify-msg " "cap not enabled") return nick = e.source.split('!')[0] auth = e.arguments[0][0] msg = e.arguments[0][1:] if not msg.startswith('#status'): return if auth != '+': self.log.debug("Ignoring message from unauthenticated " "user %s" % nick) return if nick not in self.nicks: self.log.debug("Ignoring message from untrusted user %s" % nick) return try: self.handle_command(nick, msg) except: self.log.exception("Exception handling command %s" % msg) def handle_command(self, nick, msg): parts = msg.split() command = parts[1].lower() text = ' '.join(parts[2:]) if command == 'alert': self.log.info("Processing alert from %s: %s" % (nick, text)) self.set_all_topics(text) self.broadcast('NOTICE: ' + text) for p in self.publishers: p.alert(text) elif command == 'notice': self.log.info("Processing notice from %s: %s" % (nick, text)) self.broadcast('NOTICE: ' + text) for p in self.publishers: p.notice(text) elif command == 'log': self.log.info("Processing log from %s: %s" % (nick, text)) for p in self.publishers: p.log(text) elif command == 'ok': self.log.info("Processing ok from %s: %s" % (nick, text)) self.restore_all_topics() if text: self.broadcast('NOTICE: ' + text) for p in self.publishers: p.ok(text) else: self.log.info("Unknown command %s from %s: %s" % ( command, nick, msg)) def broadcast(self, msg): for channel in self.channel_list: self.send(channel, msg) def restore_all_topics(self): t = threading.Thread(target=self._restore_all_topics, args=()) t.start() def _restore_all_topics(self): self.topic_lock.acquire() try: if self.topics: for channel in self.channel_list: self.set_topic(channel, self.topics[channel]) self.topics = {} finally: self.topic_lock.release() def set_all_topics(self, topic): t = threading.Thread(target=self._set_all_topics, args=(topic,)) t.start() def _set_all_topics(self, topic): self.topic_lock.acquire() try: if not self.topics: self.save_topics() for channel in self.channel_list: self.set_topic(channel, topic) finally: self.topic_lock.release() def save_topics(self): # Save all the current topics self.ignore_topics = False for channel in self.channel_list: self.connection.topic(channel) time.sleep(0.5) start = time.time() done = False while time.time() < start + 300: if len(self.topics) == len(self.channel_list): done = True break time.sleep(0.5) self.ignore_topics = True if not done: raise Exception("Unable to save topics") def on_currenttopic(self, c, e): if self.ignore_topics: return self.topics[e.arguments[0]] = e.arguments[1] def send(self, channel, msg): self.connection.privmsg(channel, msg) time.sleep(0.5) def set_topic(self, channel, topic): self.connection.topic(channel, topic) self.connection.privmsg('ChanServ', 'topic %s %s' % (channel, topic)) time.sleep(0.5) def _main(configpath): config = ConfigParser.ConfigParser() config.read(configpath) setup_logging(config) channels = ['#' + name.strip() for name in config.get('ircbot', 'channels').split(',')] nicks = [name.strip() for name in config.get('ircbot', 'nicks').split(',')] publishers = [StatusPage(config)] bot = StatusBot(channels, nicks, publishers, config.get('ircbot', 'nick'), config.get('ircbot', 'pass'), config.get('ircbot', 'server'), config.getint('ircbot', 'port')) bot.start() def main(): parser = argparse.ArgumentParser(description='Status bot.') parser.add_argument('-c', dest='config', nargs=1, help='specify the config file') parser.add_argument('-d', dest='nodaemon', action='store_true', help='do not run as a daemon') args = parser.parse_args() if not args.nodaemon: pid = pid_file_module.TimeoutPIDLockFile( "/var/run/statusbot/statusbot.pid", 10) with daemon.DaemonContext(pidfile=pid): _main(args.config) _main(args.config) 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()