#!/usr/bin/env ruby
require 'em-hiredis'
require 'socket'
require 'blather/client/client'
require 'chronic_duration'
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]
@logger = opts[:logger]
@redis = Flapjack::RedisPool.new(:config => @redis_config, :size => 2, :logger => @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'], Flapjack.dump_json('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: