XMPP and Ruby
Although I didn’t make it to Ezra’s RailsConf 2008 presentation on scaling Rails, I was highly interested in the topic and downloaded the slides immediately after they were available.
The big news from his presentation was the reveal of Vertebra which is billed as a Next Generation Cloud Computing/Automation Framework. One slide stood out and immediately got me thinking with the question XMPP is a realtime messaging protocol built fro IM/chat, great for communication between thousands of people, why not machines?
After a dealing with a couple of server failures at work where notification was less than satisfactory, I started mulling around the thought of using Ezra’s idea for a small scale XMPP agent that was used for server monitoring and command processing.
The result is just a small proof of concept that I put together for a recent lunch and learn demonstration. This simple XMPP agent logs into my XMPP server (I’m using Jive Software’s OpenFire) and sets its presence to available. It also immediately sends me a message saying that it’s reporting for duty.
I’ve implemented message handling in a FIFO manner with an array that acts like a queue. If you send the agent a message (using a standard XMPP client like Spark or Adium) it will reply with “Thank you for sending me the message {yourmessagehere}”. If you preface your message with command: then it will attempt to execute that command (provided that it’s in the allowed list of commands). The output of the command is then sent to the sender as an IM message.
Please remember that this is only a proof of concept and it’s not my intent to put this iteration into production.
So without further ado, the code.
require 'rubygems'
require 'xmpp4r'
include Jabber
class Agent
def initialize
user = JID.new('yourusernamehere/XMPPAgent')
@password = 'yourpasswordhere'
@client = Client.new(user)
end
def connect(server_name, port)
#Connect to server sending username and password
@client.connect(server_name, port)
@client.auth(@password)
post_connect if @client
end
def post_connect
#Set default presence to available
status = Presence.new.set_type(:available)
@client.send(status)
#Start a new queue array
@queue = []
register_callbacks
end
def disconnect
@client.close
end
def register_callbacks
@client.add_message_callback do |message|
@queue << message unless message.body.nil?
end
end
def send_message(recipient, text, reply=false)
message = Message.new(recipient)
message.type = :chat
if reply
message.body = "Thank you for sending me the message: " << text
else
message.body = text
end
@client.send(message)
end
def start_worker_thread
worker_thread = Thread.new do
puts "Started new worker thread"
#Start a loop to listen for incoming messages
loop do
if !@queue.empty?
@queue.each do |item|
puts item
#Remove the resource from the user, e.g., carlos@xmppserver/exodus = carlos@xmppserver
sender = item.from.to_s.sub(/\/.+$/, '')
#If the message included the line command: create a new command object and attempt to run it
if item.body.include? "command: "
send_message(sender, "I'll try to run " << item.body.to_s, false)
input_command = Command.new
command_result = input_command.run_command(item.body.to_s)
send_message(sender, command_result, false)
else
send_message(sender, item.body.to_s, true)
end
@queue.shift
puts "Queue is now empty" if @queue.empty?
end
end
end
sleep 1
end
worker_thread.join
end
end
class Command
@@allowable_commands = %w{ ipconfig ifconfig iisreset ping dig }
def run_command(command)
#Strip the command part out of the string - we don't need it any more.
command.slice!("command: ")
#Create an array for the arguments
arguments = command.split(" ")
arguments.delete_at(0) # Delete the first index, this is the command itself without arguments
arguments.each {|x| puts "Argument: #{x}"}
#Loop through the arguments and delete them from the command string
arguments.each {|x| command.slice!(x)}
puts "This is the command after munging #{command.strip!}"
if @@allowable_commands.include? command
puts "#{command} is an allowed command"
result = `#{command} #{arguments.join(" ")}` #Backticks are a shortcut for system("commandhere"). Join the arguments back in.
else
result = "#{command} cannot be run"
end
puts result
return result
end
end
bot = Agent.new
bot.connect("xmppserver", "5222")
bot.send_message("carlos@xmppserver", "Bot reporting for duty at #{Time.now}", false)
bot.start_worker_thread