Last month I wrote the a post titled Extending XMPP and Ruby. The post dealt with incorporating a plugin system to be used with my XMPP script that I wrote about in the post XMPP and Ruby.
To summarize the goal of the XMPP Ruby script; it’s a lightweight XMPP (Jabber/Instant Message) agent that resides on servers. Using this agent, I can immediately tell if a server is up and running as well as pass it commands through my IM client and have it reply back to me with the output. To enforce security, the script had a set number of commands that it could execute.
This had the problem of not really being scalable or extensible. For example, in order to add allowed commands one would have to modify the actual XMPP script itself. Not good.
In order to rectify that, I authored the plugin system that dynamically loaded classes at start time. This made it possible for me to be able to instantiate any object from those classes and run the methods of those classes.
In the test script it worked fine, but when incorporated with the actual XMPP agent script it was still hobbled by the fact that if you wanted to instantiate a new object you’d have to modify the original script. Again, not scalable or extensible.
With this latest iteration, I have added the ability to dynamically instantiate objects based on plugins as well as the ability to call the methods of the object all from the IM interface.

Before we explore the code, let’s take a look at our file structure (pictured in Figure 1). Everything is contained in a top level directory titled xmpp_agent. There is an init.rb file which loads all of the plugins in the plugins directory which are: command.rb, iis.rb, network.rb, system.rb. Below the plugins, are the files test.rb and xmpp_agent.rb.
The test.rb file is a “scratchpad” file that I use to test functionality. It is not a unit test file. As the figure mentions, the command.rb plugin file is new to this iteration. This file is responsible for creating objects dynamically at runtime as well as dynamically executing methods from those objects.
Since running commands have been abstracted out to its own class, there is no need to have that resident in the xmpp_agent.rb file. That code has been removed from the previous incarnation (see XMPP and Ruby). Everything else remains the same in the xmpp_agent.rb file with the exception of a new require declaration at the top of the script; require ‘init’ – as this loads our plugins (see Extending XMPP and Ruby).
When you send the agent a message that is phrased like this (items in brackets are variables):
command: [plugin] [method name] [arguments]
It will create a new Command object and calls the run_command method.
The first thing that the run_command method does is it calls a command parser method shown below:
def parse_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(" ")
return arguments
end
This removes the extraneous “command:” part of the IM message and returns the rest of the string to the run_command method which is defined below:
def run_command(command)
vars = self.parse_command(command)
#Convert the first item in the array to a class
begin
class_name = Object.const_get(vars[0].capitalize)
rescue Exception => e
return "ERROR: " << e.message << ". Are you sure that the #{vars[0]} plugin is installed?"
end
#Remove the first item (class name) since it's not needed anymore
vars.delete_at(0)
#Get the method name and remove it from the array
if vars[0]
method_name = vars.delete_at(0)
else
return "ERROR: Are you missing the method name?"
end
#If there are more arguments left call remote method passing in args, else just call the remote method
if vars[0]
run_remote_method(class_name, method_name, vars.each {|x| "#{x}" })
else
run_remote_method(class_name, method_name)
end
end
Now that we have everything parsed like we need it, we call run_remote_method.
def run_remote_method(class_name, method_name, *args)
begin
o = class_name.new
rescue Exception => e
return "ERROR: " << e.message << ". Can't create the object, is it in the plugin folder?"
end
begin
o.send(method_name, *args)
rescue Exception => e
return "ERROR: " << e.message << ". Did you include this method in the plugin file?"
end
end
As you can see, we dynamically instantiate a new object based on the class name that we’re passing in. After the exception handling, we call the method of the new object followed by any arguments.
For example: Let’s say that I want my server to ping an address. Instead of logging into the server and running the ping command, I could use the IM client to tell the server to ping the address and give me the results. All I have to type into the IM chat window is the following: command: network ping -c2 eddorre.com. This will ping eddorre.com twice on *nix machines.
The script will dynamically instantiate a Network object (provided it’s in the plugins directory) and try to run the method ping followed by any arguments.
In order for this to work, the network plugin/class has to have a method for ping in it. Let’s take a look at the code for that:
def ping(*args)
command = 'ping ' << args.join(" ")
#Execute the command and return the result
`#{command}`
end
From the code above, I have defined a method called ping which takes the arguments at the command line, appends them to “ping”, and then executes the command (the backtick symbols mean execute the command and return the result).
Now, I’ll admit, using IM as an interactive shell is sort of limiting, but it can be much more powerful. For example, let’s take a look at the IIS class. This class, would in theory, handle everything related to Microsoft’s IIS web server. Let’s say that we want to find out how many current anonymous users are on a specific web site on our web server. We can define a method to return that information via the IM interface.
For example, let’s say I call this from the IM interface: command: iis current_anon_users [website]
The code for the current_anon_users method is below:
require 'win32ole'
class Iis < Plugin
def current_anon_users(site)
wmi = WIN32OLE.connect("winmgmts:root\\cimv2:Win32_PerfRawData_W3SVC_WebService.Name='_#{site}'")
wmi.CurrentAnonymousUsers
end
end
The code is simple, use a WMI object to return the data using the built in performance monitor on the server.
Using this new extensibility, you would be able to send a message to an agent such as “record this show” or “turn on the lights” depending on what you wanted it to do. All you have to do is build the plugin for it.