#!/usr/bin/env ruby require 'em-hiredis' require 'socket' require 'blather/client/client' require 'chronic_duration' require 'oj' require 'flapjack/data/entity_check' require 'flapjack/redis_pool' require 'flapjack/utility' require 'flapjack/version' require 'flapjack/data/alert' module Flapjack module Gateways class Jabber < Blather::Client include Flapjack::Utility log = ::Logger.new(STDOUT) log.level = ::Logger::INFO Blather.logger = log # TODO if we use 'xmpp4r' rather than 'blather', port this to 'rexml' class TextHandler < Nokogiri::XML::SAX::Document def initialize @chunks = [] end attr_reader :chunks def cdata_block(string) characters(string) end def characters(string) @chunks << string.strip if string.strip != "" end end def initialize(opts = {}) @config = opts[:config] @redis_config = opts[:redis_config] || {} @boot_time = opts[:boot_time] @redis = Flapjack::RedisPool.new(:config => @redis_config, :size => 2) @logger = opts[:logger] @logger.debug("Jabber Initializing") @buffer = [] @hostname = Socket.gethostname # FIXME: i suspect the following should be in #setup so a config reload updates @identifiers # I moved it here so the rspec passes :-/ @alias = @config['alias'] || 'flapjack' @identifiers = ((@config['identifiers'] || []) + [@alias]).uniq @logger.debug("I will respond to the following identifiers: #{@identifiers.join(', ')}") super() end def stop @should_quit = true redis_uri = @redis_config[:path] || "redis://#{@redis_config[:host] || '127.0.0.1'}:#{@redis_config[:port] || '6379'}/#{@redis_config[:db] || '0'}" shutdown_redis = EM::Hiredis.connect(redis_uri) shutdown_redis.rpush(@config['queue'], Oj.dump('notification_type' => 'shutdown')) end def setup jid = @config['jabberid'] || 'flapjack' jid += '/' + @hostname unless jid.include?('/') @flapjack_jid = Blather::JID.new(jid) super(@flapjack_jid, @config['password'], @config['server'], @config['port'].to_i) @logger.debug("Building jabber connection with jabberid: " + @flapjack_jid.to_s + ", port: " + @config['port'].to_s + ", server: " + @config['server'].to_s + ", password: " + @config['password'].to_s) register_handler :ready do |stanza| EventMachine::Synchrony.next_tick do on_ready(stanza) end end body_matchers = @identifiers.inject([]) do |memo, identifier| @logger.debug("identifier: #{identifier}, memo: #{memo}") memo << {:body => /^#{identifier}[:\s]/} memo end @logger.debug("body_matchers: #{body_matchers}") register_handler :message, :groupchat?, body_matchers do |stanza| EventMachine::Synchrony.next_tick do on_groupchat(stanza) end end register_handler :message, :chat?, :body do |stanza| EventMachine::Synchrony.next_tick do on_chat(stanza) end end register_handler :disconnected do |stanza| ret = true EventMachine::Synchrony.next_tick do ret = on_disconnect(stanza) end ret end end # Join the MUC Chat room after connecting. def on_ready(stanza) return if @should_quit @connected_at = Time.now.to_i @logger.info("Jabber Connected") if @config['rooms'] && @config['rooms'].length > 0 @config['rooms'].each do |room| @logger.info("Joining room #{room}") presence = Blather::Stanza::Presence.new presence.from = @flapjack_jid presence.to = Blather::JID.new("#{room}/#{@alias}") presence << "" EventMachine::Synchrony.next_tick do write presence say(room, "flapjack jabber gateway started at #{Time.now}, hello! Try typing 'help'.", :groupchat) end end end return if @buffer.empty? while stanza = @buffer.shift @logger.debug("Sending a buffered jabber message to: #{stanza.to}, using: #{stanza.type}, message: #{stanza.body}") EventMachine::Synchrony.next_tick do write(stanza) end end end def get_check_details(entity_check, current_time) sched = entity_check.current_maintenance(:scheduled => true) unsched = entity_check.current_maintenance(:unscheduled => true) out = '' if sched.nil? && unsched.nil? out += "Not in scheduled or unscheduled maintenance.\n" else if sched.nil? out += "Not in scheduled maintenance.\n" else start = Time.at(sched[:start_time]) finish = Time.at(sched[:start_time] + sched[:duration]) remain = time_period_in_words( (finish - current_time).ceil ) # TODO a simpler time format? out += "In scheduled maintenance: #{start} -> #{finish} (#{remain} remaining)\n" end if unsched.nil? out += "Not in unscheduled maintenance.\n" else start = Time.at(unsched[:start_time]) finish = Time.at(unsched[:start_time] + unsched[:duration]) remain = time_period_in_words( (finish - current_time).ceil ) # TODO a simpler time format? out += "In unscheduled maintenance: #{start} -> #{finish} (#{remain} remaining)\n" end end out end def interpreter(command_raw, from) msg = nil action = nil entity_check = nil th = TextHandler.new parser = Nokogiri::HTML::SAX::Parser.new(th) parser.parse(command_raw) command = th.chunks.join(' ') case command when /^ACKID\s+([0-9A-F]+)(?:\s*(.*?)(?:\s*duration:.*?(\w+.*))?)$/im ackid = $1 comment = $2 duration_str = $3 error = nil dur = nil if comment.nil? || (comment.length == 0) error = "please provide a comment, eg \"#{@config['alias']}: ACKID #{$1} AL looking\"" elsif duration_str # a fairly liberal match above, we'll let chronic_duration do the heavy lifting dur = ChronicDuration.parse(duration_str) end four_hours = 4 * 60 * 60 duration = (dur.nil? || (dur <= 0)) ? four_hours : dur event_id = @redis.hget('checks_by_hash', ackid) if event_id.nil? error = "not found" else entity_check = Flapjack::Data::EntityCheck.for_event_id(event_id, :redis => @redis) error = "unknown entity" if entity_check.nil? end if error msg = "ERROR - couldn't ACK #{ackid} - #{error}" else entity_name, check = event_id.split(':', 2) if entity_check.in_unscheduled_maintenance? # ack = entity_check.current_maintenance(:unscheduled => true) # FIXME details from current? msg = "Changing ACK for #{check} on #{entity_name} (#{ackid})" else msg = "ACKing #{check} on #{entity_name} (#{ackid})" end action = Proc.new { Flapjack::Data::Event.create_acknowledgement( entity_name, check, :summary => (comment || ''), :acknowledgement_id => ackid, :duration => duration, :redis => @redis ) } end when /^help$/i msg = "commands: \n" + " ACKID [duration: