Select Page

Ruby Plugin Architectures

Most of the applications I write in Ruby are some kind of Framework, ruby-pdns takes plugins, mcollective takes plugins, my nagios notification bot takes plugins etc, yet I have not yet figured out a decent approach to handling plugins.

Google suggests many options, the most suggested one is something along these lines.

class Plugin
    def self.inherited(klass)
        PluginManager << klass.new
    end
end
 
class FooPlugin<Plugin
end

Where PluginManager is some class or module that stores and later allows retrieval, when the FooPlugin class gets created it will trigger the hook in the base class.

This works ok, almost perfectly, except that at the time of the trigger the FooPlugin class is not 100% complete and your constructor will not be called, quite a pain. From what I can tell it calls the constructor on either Class or Object.

I ended up tweaking the pattern a bit and now have something that works well, essentially if you pass a String to the PluginManager it will just store that as a class name and later create you an instance of that class, else if it’s not a string it will save it as a fully realized class assuming that you know what you did.

The full class is part of mcollective and you can see the source here but below the short version:

I am quite annoyed that including a module does not also include static methods in Ruby, its quite a huge miss feature in my view and there are discussions about changing that behavior. I had hopes of writing something simple that I can just do include Pluggable and this would set up all the various bits, create the inherited hook etc, but it’s proven to be a pain and would be littered with nasty evals etc.

module PluginManager
   @plugins = {}
 
   def self.<<(plugin)
         type = plugin[:type]
         klass = plugin[:class]
 
         raise("Plugin #{type} already loaded") if @plugins.include?(type)
 
         if klass.is_a?(String)
             @plugins[type] = {:loadtime => Time.now, :class => klass, :instance => nil}
         else
             @plugins[type] = {:loadtime => Time.now, :class => klass.class, :instance => klass}
         end
   end
 
   def self.[](plugin)
       raise("No plugin #{plugin} defined") unless @plugins.include?(plugin)
 
       # Create an instance of the class if one hasn't been done before
       if @plugins[plugin][:instance] == nil
            begin
                 klass = @plugins[plugin][:class]
                 @plugins[plugin][:instance] = eval("#{klass}.new")
            rescue Exception => e
                raise("Could not create instance of plugin #{plugin}: #{e}")
            end
       end
 
       @plugins[plugin][:instance]
   end
end
 
class Plugin
   def self.inherited(klass)
      PluginManager << {:type => "facts_plugin", :class => klass.to_s}
   end
end
 
class FooPlugin<Plugin
end

For mcollective I only ever allow one of a specific type of plugin so the code is a bit specific in that regard.

I think late creating the plugin instances is quite an improvement too since often you’re loading in plugins that you just don’t need like client apps would probably not need a few of the stuff I load in and creating instances is just a waste.

I am not 100% sold on this approach as the right one, I think I’ll probably refine it more and would love to hear what other people have done.

This has though removed a whole chunk of grim code from mcollective since I now store all plugins and agents in here and just fetch them as needed. So already this is an improvement to what I had before so I guess it works well and should be easier to refactor for improvements now.

Managing puppetd with mcollective

It’s typical during maintenance windows that you would want to disable puppet, do your work, enable again and do a run. Or perhaps you don’t run puppet all the time, you just want to kick it off during your maintenance window. Doing this with ssh for loops is slow and annoying, here’s a way to target large sums of machines for these actions using mcollective.

Using mcollective‘s discovery features and a suitable agent this is really easy, I’ve written such an agent and made it available on the mcollective-plugins site.

You can see below a sample session with it. In all of the examples below we’re constraining it to hosts with the roles::dev_server puppet class using mcollective discovery. Not shown here is that you can get status as well as use the splay options provided by puppet, see the wiki page for details on that.

First we’ll make sure it’s enabled.

$ mc-puppetd --with-class roles::dev_server enable
Determining the amount of hosts matching filter for 2 seconds .... 1
 
.
 
Finished processing 1 / 1 hosts in 9.81 ms

Now we’ll disable it

$ mc-puppetd --with-class roles::dev_server disable
Determining the amount of hosts matching filter for 2 seconds .... 1
 
.
 
Finished processing 1 / 1 hosts in 3252.13 ms

We’ll attempt a runonce, this should fail because we just disabled the agent.

$ mc-puppetd --with-class roles::dev_server runonce -v
Determining the amount of hosts matching filter for 2 seconds .... 1
 
dev1.your.net                      status=false
    Lock file exists                        
 
 
---- puppetd agent stats ----
           Nodes: 1 / 1
      Start Time: Sun Nov 29 23:02:30 +0000 2009
  Discovery Time: 2006.38ms
      Agent Time: 47.62ms
      Total Time: 2054.00ms

Let’s enable it and then try to run again.

$ mc-puppetd --with-class roles::dev_server enable
Determining the amount of hosts matching filter for 2 seconds .... 1
 
.
 
Finished processing 1 / 1 hosts in 9.81 ms
 
$ mc-puppetd --with-class roles::dev_server runonce
Determining the amount of hosts matching filter for 2 seconds .... 1
 
.
 
Finished processing 1 / 1 hosts in 2801.82 ms

I think this is a good way to orchestrate these type of maintenance window and I hope someone finds it useful.

Registration in MCollective

Since rolling out mcollective to more and more machines I sometimes noticed one or two weren’t checking in and found it hard to figure out which ones it was. One person evaluating it also expressed interest in some form of registration ability so that they can build up an inventory of what is out there using mcollective.

At first it seemed a bit against what I set out to do – no central database, use discovery instead – but I think the two compliment each other well, I still use discovery to actually interact with the network, registration is there to assist in building web interfaces or other inventories.

I added the ability to call a configurable plugin at a configurable interval, basically whatever data your plugin returns will be sent to the collective directed at an agent ‘registration’. A sample plugin is provided, it simply returns a list of agents as an array and you can see how trivial it is to write your own.

Using the registration system I wrote a plugin that simply keeps a file in a directory for each member and a simple nagios check will then report if there are any files older than registration interval + 30. It’s quite simple but works well, the moment one of my machines goes silent the monitor goes red.

You can grab the agent and monitor script here.

Note that whatever work your registration agent will do need to be fast, you’ll be getting a large amount of registration messages from all over your network so if you take many seconds to process each you’ll run into problems. You can get some more details about registration on the wiki page

RightScale facts

I’m trying to build up a nice demo of mcollective and trying to save some effort by using the RightScale CentOS AMI’s.ย  I noticed they came with a nice script to pull down the user data and meta data so figured I might as well make some facts.

require 'find'
 
if File.exists?("/var/spool/ec2/meta-data")
    Find.find("/var/spool/ec2/meta-data") do |path|
        filename = File.basename(path)
        factname = "ec2_#{filename}"
 
        factname.gsub!(/-/, "_")
 
        if File.file?(path)
            lines = File.readlines(path)
 
            if lines.size == 1
                Facter.add(factname) do
                    setcode { lines.first.chomp.to_s }
                end
            else
                lines.each_with_index do |line, i|
                    Facter.add("#{factname}_#{i}") do
                        setcode { lines[i].chomp }
                    end
                end
            end
        end
    end
end
 
if File.exists?("/var/spool/ec2/user-data.raw")
        lines = File.readlines("/var/spool/ec2/user-data.raw")
 
        lines.each do |l|
                if l.chomp =~ /(.+)=(.+)/
                    f = $1; v = $2
 
                    Facter.add(f) do
                        setcode { v }
                    end
                end
        end
end

If you arrange to run /opt/rightscale/bin/ec2.sh in rc.local and pop this fact above into your factdir you should be able to access all the meta data from facter.

# facter -p
ec2_ami_id => ami-73270c07
ec2_ami_launch_index => 0
ec2_ami_manifest_path => pinetecltd-centos-clustera/cluster-webserver-1257783713.manifest.xml
ec2_ancestor_ami_ids_0 => ami-31c72258
ec2_ancestor_ami_ids_1 => ami-ef01e486
ec2_ancestor_ami_ids_2 => ami-0916f360
ec2_ancestor_ami_ids_3 => ami-c8ac48a1
ec2_ancestor_ami_ids_4 => ami-cd52b6a4
ec2_ancestor_ami_ids_5 => ami-19be966d
ec2_ancestor_ami_ids_6 => ami-65200b11
ec2_ancestor_ami_ids_7 => ami-3d200b49
ec2_ancestor_ami_ids_8 => ami-91200be5
ec2_ancestor_ami_ids_9 => ami-81200bf5
ec2_block_device_mapping_ami => sda1
ec2_block_device_mapping_ephemeral0 => sdb
ec2_block_device_mapping_ephemeral1 => sdc
ec2_block_device_mapping_ephemeral2 => sdd
ec2_block_device_mapping_ephemeral3 => sde
ec2_block_device_mapping_root => /dev/sda1
ec2_block_device_mapping_swap => sda3
ec2_hostname => ip-10-227-43-134.eu-west-1.compute.internal
ec2_instance_action => none
ec2_instance_id => i-9411e7e3
ec2_instance_type => m1.small
ec2_kernel_id => aki-7e0d250a
ec2_local_hostname => ip-10-227-43-134.eu-west-1.compute.internal
ec2_local_ipv4 => 10.227.43.134
ec2_placement_availability_zone => eu-west-1b
ec2_public_hostname => ec2-79-125-33-224.eu-west-1.compute.amazonaws.com
ec2_public_ipv4 => 79.125.33.224
ec2_public_keys_0_openssh_key => ssh-rsa AAA
ec2_ramdisk_id => ari-7d0d2509
ec2_reservation_id => r-c655bab1
ec2_security_groups_0 => rip
ec2_security_groups_1 => defaultcluster => a

In addition if you just pass nice key=val pairs in as user data it will add those as facts too, the last above is from that.

The Marionette Collective

Some time ago I posted a blog post and screencast about my middleware solution to concurrent systems administration, I had a lot of interest in this and in the end relented and released the thing as Open Source. You should go watch the screencast to really grasp what is going on and what this achieves.

I asked around on Twitter for name suggestions and got many great suggestions, in the end I settled on mcollective short for The Marionette Collective, yeah it sounds like an Emo band, I don’t care. It’ll be easy to google and I got a matching .org domain, that helps over certain other ungoogleable project names!

The code for the core service is released under the Apache License version 2 and is available at google code: http://marionette-collective.org/

There’s a wiki getting started guide and also a quick guide on writing agents and CLI tools to talk to it.

By default the code includes the following plugins:

  • Security by means of a Pre Shared Key
  • Communications with Stomp compatible servers
  • Facts about machines via a simple YAML file
  • Discovery

You can drop in replacement agents for all of the above and perhaps write something to use full SSL encryption etc. No actual agents to do any real work is included in the core Apache Licensed code.

A second project was started where I’m dropping a bunch of my own agents, the projects are separate because the agents might have different licensing from the core app server, for example there’s an agent to use Facter from Reductive Labs for facts but this means the code has to be GPL.

Agents available right now lets you use facter for facts, manage services using the puppet providers and also do distributed url benchmarks, check out the plugins project: http://code.google.com/p/mcollective-plugins/

I’d encourage others to put agent on Github or wherever and to announce it on the users list or just to drop me a mail and I’ll link to it from the project wiki – you can also join one of the projects and just commit your code yourself.

It’s still early days – I was accused of being too perfectionist in how I like to release code, so this is very much an early and often approach to releasing! The entire code base is about 3 weeks old and I spent mostly some free time hacking it up, so there’s much improvement to be made still, I do however use a version of it in production and find it very stable and reliable so far.

I am looking for early testers to give me feedback about the code, structure of the project etc. If you’re stuck grab me on freenode my nick is Volcane and I’ll see if I can help you get it going.

Using Ruby Net::IMAP with plain auth

I’ve had to help a client pull out sender addresses from a folder on a Zimbra server, Ruby supports IMAP but only with LOGIN and CRAM-MD5 methods while Zimbra wants PLAIN. Net::IMAP supports adding a new authenticator so it was pretty simple in the end:

class ImapPlainAuthenticator
  def process(data)
    return "#{@user}\0#{@user}\0#{@password}"
  end
 
  def initialize(user, password)
    @user = user
    @password = password
  end
end
 
Net::IMAP::add_authenticator('PLAIN', ImapPlainAuthenticator)

And using it to retrieve the list of senders:

imap = Net::IMAP.new('imap.your.com')
 
imap.authenticate('PLAIN', 'username', 'pass')
imap.examine('INBOX/subfolder')
imap.search(["ALL"]).each do |message_id|
    envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"]
    puts "#{envelope.from[0].mailbox}"
end

Very simple, took about 5 minutes and saved days of manual pain and suffering, result!