Eddorre.com

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


Comments

Add Your Comment





end kanji