www.devco.net by r.i.pienaar

12Mar/100

Puppet Concat 20100312

I am pleased to announce the next version of my Puppet Concat script, we now have 0.24.8 and newer support and a few smaller bits mentioned below.

For background of what this is about please see my earlier post: Building files from fragments with Puppet

New in this release

Paul Elliot sent in most of the patches that enabled this release, lots of thanks Paul!

  • 0.24.8 and newer is supported
  • You can now prepend warnings to generated files as a shell style comment using the warn property
  • You can enable the ability to create empty concat files using the force property
  • You can configure the location of your sort binary in setup.pp

The code should auto configure for 0.24.8 use, if it does not work please see setup.pp.

You can grab the code here.

Known issues

As with my earlier attempts at making a concat tool for 0.24.x this version when used on 0.24 will raise some false notifies. Basically the method we use to clear the concat store of unmanaged files has a side effect and on the next run you will get an unneeded notify. Puppets behavior has improved in 0.25 so it works as expected there, for 0.24 though there is no known work around.

You cannot change the owner of a file, I know how to work around this issue and will have something in the next release.

Tagged as: , No Comments
26Feb/104

What does Puppet manage on a node?

Last year I wrote a tool to parse the localconfig.yaml from Puppet 0.24 and display a list of resources and classes. This script failed when 0.25 came out, I've updated it for 0.25 support.

The yaml cache has some added features in 0.25 so now I can also show the list of tags on a node, output would be:

# parselocalconfig.rb /var/lib/puppet/client_yaml/catalog/fqdn.yaml
Classes included on this node:
        fqdn
        common::linux
        <snip>
 
Tags for this node:
        fqdn
        common::linux
        <snip>
 
Resources managed by puppet on this node:
        yumrepo{centos-base: }
                defined in common/modules/yum/manifests/init.pp:24
 
        file{/root/.ssh: }
                defined in common/modules/users/manifests/root.pp:20
 
        <snip>

You can get the script that supports both 0.24 and 0.25 here.

Tagged as: , , 4 Comments
5Feb/102

Adding methods to a ruby class

I'm just blogging this because it took me ages to figure out, it seems so simple now but I guess that's how it usually goes.

The problem I have is I want a plugin to be able to either make a method using the normal Ruby def foo or via some DSL'ish helpers.

class Foo<Base
   register_action(:name => "do_something", :description => "foo")
 
   def do_something_action
   end
 
   register_action(:name => "do_something_else", :description => "foo") do
      # body of the action here
   end
end

The above code should make me two methods - do_something_action and do_something_else_action - they should be identical to viewers from the outside. Here's the base class that makes this happen correctly:

class Base
   def self.register_input(input, &block)
      name = input[:name]
 
      self.module_eval { define_method("#{name}_action", &block) } if block_given?
   end
end

It's pretty simple, we're just using define_method in the scope of the module and that does the rest.

Tagged as: , 2 Comments
3Feb/100

MCollective Agent Introspection

With the new SimpleRPC system in MCollective we have a simple interface to creating agents. The way to call an agent would be:

$ mc-rpc service status service=httpd

This is all fine and well and easy enough, however it requires you to know a lot. You need to know there's a status action and you need to know it expects a service argument, not great.

I'm busy adding the ability for an agent to register its metadata and interface so that 3rd party tools can dynamically generate useful interfaces.

A sample registration for service agent is:

register_meta(:name        => "SimpleRPC Service Agent",
              :description => "Agent to manage services using the Puppet service provider",
              :author      => "R.I.Pienaar",
              :license     => "GPLv2",
              :version     => 1.1,
              :url         => "http://mcollective-plugins.googlecode.com/",
              :timeout     => 60)
 
["start", "stop", "restart", "status"].each do |action|
    register_input(:action      => action,
                   :name        => "service",
                   :prompt      => "Service Name",
                   :description => "The service to #{action}",
                   :type        => :string,
                   :validation  => '^[a-zA-Z\-_\d]+$',
                   :maxlength   => 30):

This includes all the meta data, versions, timeouts, validation of inputs, prompts and help text for every input argument.

Using this we can now generate dynamic UI's, and do something like JavaDoc generated documentation. I've recorded a little video demonstrating a proof of concept Text UI that uses this data to generate a UI dynamically. This is ripe for integration into tools like Foreman and Puppet Dashboard.

Please watch the video here, best viewed full screen.

2Jan/100

MCollective Release 0.4.x

A few days ago I released Marionette Collective version 0.4.0 and today I released 0.4.1. This release branch introduce a major new feature called Simple RPC.

In prior releases it took quite a bit of ruby knowledge to write a agent and client. In addition clients all ended up implementing their own little protocols for data exchange. We've simplified agents and clients and we've created a standard protocol between clients and agents.

Standard protocols between clients and agents means we have a standard one-size-fits-all client program called mc-rpc and it opens the door to writing simple web interfaces that can talk to all compliant agents. We've made a test REST <-> Simple RPC bridge as an example.

Writing a client can now be done without all the earlier setup, command line parsing and so forth, it can now be as simple as:

require 'mcollective'
 
include MCollective::RPC
 
mc = rpcclient("rpctest")
 
printrpc mc.echo(:msg => "Welcome to MCollective Simple RPC")
 
printrpcstats

This simple client has full discovery, full --help output, and takes care of printing results and stats in a uniform way.

This should make it much easier to write more complex agents, like deployers that interact with packages, firewalls and services all in a single simple script.

We've taken a better approach in presenting the output from clients now, instead of listing 1000 OKs on success it will now only print whats failing.

Output from above client would look something along these lines:

$ hello.rb
 
 * [ ============================================================> ] 43 / 43
 
Finished processing 43 / 43 hosts in 392.60 ms

As you can see we have a nice progress indicator that will work for 1 or 1000 nodes, you can still see status of every reply by just running the client in verbose - which will also add more detailed stats at the end.

Agents are also much easier, here's a echo agent:

class Rpctest<RPC::Agent
    def echo_action
         validate :msg, :shellsafe
 
         reply.data = request[:msg]
    end
end

You can get full information on this new feature here. We've also created a lot of new wiki docs about ActiveMQ setup for use with MCollective and we've recorded a new introduction video here.

14Dec/090

Exim, MCollective and speed

Usually when I describe mcollective to someone they generally think its nice and all but the infrastructure to install is quite a bit and so ssh parallel tools like cap seems a better choice. They like the discovery and stuff but it's not all that clear.

I have a different end-game in mind than just restarting services, and I've made a video to show just how I manage a cluster of Exim servers using mcollective. This video should give you some ideas about the possibilities that the architecture I chose brings to the table and just what it can enable.

While watching the video please note how quick and interactive everything is, then keep in mind the following while you are seeing the dialog driven app:

  • I am logged in via SSH from UK to Germany into a little VM there
  • The mcollective client talks to a Germany based ActiveMQ
  • The 4 mail servers in the 2nd part of the demo are based 2 x US, 1 x UK and 1 x DE
  • I have ActiveMQ instances in each of the above countries clustered together using the technique previous documented here.

Here's the video then, as before I suggest you hit the full screen link and watch it that way to see what's going on.




This is the end game, I want a framework to enable this kind of tool on Unix CLI - complete with pipes as you'd expect - things like the dialog interface you see here, on the web, in general shell scripts and in nagios checks like with cucumber-nagios, all sharing a API and all talking to a collective of servers as if they are one. I want to make building these apps easy, quick and fun.

14Dec/091

Splitting MySQL dumps by table – take 2

A few days ago I posted about splitting mysqldump files using sed and a bit of Ruby to drive it, turns out that sucked, a lot.

I eventually killed it after 2 days of not finishing, the problem is, obviously, that sed does not seek to the position, it reads the whole file. So pulling out the last line of a 150GB file requires reading 150GB of data, if you have 120 tables this is a huge problem.

The below code is a new take on it, I am just reading the file with ruby and spitting out the resulting files with 1 read operation, start to finish on the same data was less than a hour. When run it gives you nice output like this:

Found a new table: sms_queue_out_status
    writing line: 1954 2001049770 bytes in 91 seconds 21989557 bytes/sec
 
Found a new table: sms_scheduling
    writing line: 725 729256250 bytes in 33 seconds 22098674 bytes/sec

The new code below:

#!/usr/bin/ruby
 
if ARGV.length == 1
    dumpfile = ARGV.shift
else
    puts("Please specify a dumpfile to process")
    exit 1
end
 
STDOUT.sync = true
 
if File.exist?(dumpfile)
    d = File.new(dumpfile, "r")
 
    outfile = false
    table = ""
    linecount = tablecount = starttime = 0
 
    while (line = d.gets)
        if line =~ /^-- Table structure for table .(.+)./
            table = $1
            linecount = 0
            tablecount += 1
 
            puts("\n\n") if outfile
 
            puts("Found a new table: #{table}")
 
            starttime = Time.now
            outfile = File.new("#{table}.sql", "w")
        end
 
        if table != "" && outfile
            outfile.syswrite line
            linecount += 1
            elapsed = Time.now.to_i - starttime.to_i + 1
            print("    writing line: #{linecount} #{outfile.stat.size} bytes in #{elapsed} seconds #{outfile.stat.size / elapsed} bytes/sec\r")
        end
    end
end
 
puts
Tagged as: , , 1 Comment
11Dec/090

Splitting MySQL dumps by table

I often need to split large mysql dumps into smaller files so I can do selective imports from live to dev for example where you might not want all the data. Each time I seem to rescript some solution for the problem. So here's my current solution to the problem, it's a simple Ruby script, you give it the path to a mysqldump and it outputs a string of echo's and sed commands to do the work.

UPDATE: Please do not use this code, it's too slow and inefficient, new code can be found here.

Just pipe it's output to a file and run it via shell when you're ready to do the splitting. At the end you'll have a file per table in your cwd.

#!/usr/bin/ruby
 
prevtable = ""
prevline = 0
 
if ARGV.length == 1
    dumpfile = ARGV.shift
else
    puts("Please specify a dumpfile to process")
    exit 1
end
 
if File.exist?(dumpfile)
   %x[grep -n "Table structure for table" #{dumpfile}].each do |line|
       if line =~ /(\d+):-- Table structure for table .(.+)./
           curline = $1.to_i
           table = $2
 
           unless prevtable == ""
               puts("echo \"\`date\`: Processing #{prevtable} - lines #{prevline - 1} to #{curline - 2}\"")
               puts("sed -n '#{prevline - 1},#{curline - 2}p;#{curline - 2}q' #{dumpfile} > #{prevtable}.sql")
               puts
           end
 
           prevline = curline
           prevtable = table
       end
   end
else
   puts("Can't find dumpfile #{dumpfile}")
   exit 1
end

It's pretty fast, the heavy lifting is all done with grep and sed, ruby just there to drive those commands and parse a few lines of output.

Running it produces something like this:

$ split-mysql-dump.rb exim.sql
echo "`date`: Processing domain_sender_whitelist - lines 32 to 47"
sed -n '32,47p;47q' exim.sql > domain_sender_whitelist.sql
 
echo "`date`: Processing domain_valid_users - lines 48 to 64"
sed -n '48,64p;64q' exim.sql > domain_valid_users.sql
Tagged as: , , No Comments
1Dec/090

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.

30Nov/090

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.