Embedding MCollective

MCollective is “a framework to build server orchestration or parallel job execution systems”. It’s very easy to create and deploy agents to your hosts, and then interrogate or instruct them with simple command-line tools. For ad hoc tasks, this is ideal.

As part of our platform engineering work though, we’re integrating MCollective with existing tools, and with our new orchestration systems – using MCollective as the glue between a centralised controller and its agents on individual hosts.

In this situation, our “client” isn’t a command-line tool but a larger Rails application with a number of other dependencies. Bringing the mcollective tools into this environment isn’t straightforward, since a number of the default options need to be changed.

Here’s my first attempt to embed an MCollective client in my application:

require 'mcollective'

class EmbeddedClient
  include MCollective::RPC
   
  def self.client(agent)
    client = rpcclient(agent)
  end
end

This has a number of problems.

I need to make the “mcollective” library code available. There’s currently no gem available, so it’s not possible to just gem "mcollective", "1.2.0" in the Gemfile. There’s also the MCollective plugins, which need to be in sync with the core library version.

The configuration also needs to be available, and the default location is /etc/mcollective/client.cfg - which is naturally outside of any deployed application’s root, and so we’d need to deploy that separately.

Having made the code available, the next problem to deal with is command-line option parsing. By default, MCollective will parse and validate the process’s ARGV, and raise an exception if it finds an option it doesn’t recognise. This isn’t appropriate in an embedded client, so we need to bypass option parsing.

Here’s how to create an embedded MCollective client, taking these points into account:

require 'mcollective'

class EmbeddedClient
  include MCollective::RPC
   
  def self.client(agent)
    options =  MCollective::Util.default_options
    options[:config] = 'config/mcollective.cfg'
    client = rpcclient(agent, {:options => options})
    client.discovery_timeout = 10
    client.timeout = 120
    client
  end
end

and here’s the configuration that points to:

topicprefix = /topic/
main_collective = mcollective
collectives = mcollective
libdir = vendor/mcollective/plugins
logfile = stdout
loglevel = debug

# Plugins
securityprovider = psk
plugin.psk = yeah

connector = stomp
plugin.stomp.host = 10.101.1.16
plugin.stomp.port = 61613
plugin.stomp.user = guest
plugin.stomp.password = guest

# Facts
factsource = yaml
plugin.yaml = config/mcollective_facts.yaml

This works, but there’s a problem - in rpcclient(), there’s a call to exit! on any exception during the client setup process, including exceptions thrown by the stomp library. We need to avoid that and handle exceptions ourselves, so:

require 'mcollective'

class EmbeddedClient
  include MCollective::RPC

  def self.client(agent)
    options =  MCollective::Util.default_options
    options[:config] = 'config/mcollective.cfg'
    client = MCollective::RPC::Client.new(agent, :options => options)
    client.discovery_timeout = 10
    client.timeout = 120
    client
  end
end

Now we can handle exceptions ourselves, but there’s still an issue - the stomp library writes directly to $stderr and we should be collecting that output and logging it in whatever way is appropriate for the application:

def log_stderr &block
  begin
    real_stderr, $stderr = $stderr, StringIO.new
    yield
  ensure
    $stderr, stderr = real_stderr, $stderr
    stderr.string.each_line do |line|
      log(:error, "stderr: #{line}")
    end
  end
end

# and then:
log_stderr do
  run_mcollective_processes
end

At this point, we’ve got the following problems solved:

which should let the application interact with MCollective in a reliable and maintainable way.