> node :node1 do
>   ip 192.whatever
>   title "Node 1"
> end
>
> node :node2 do
>   ip 192.whatever
>   title "Node 2"
> end

<snip>

> class ClusterManager
> 
>   def load_config_file config_file
>     instance_eval File.read(config_file)
>   end
> 
>   def node node_id, &block
>     # What goes here?
>   end
> 
> end

I'd think similarly, but a bit different in that
ClusterManager::Builder should be the one doing the instance eval:

class ClusterManager
  attr_accessor :nodes

  def initialize
    @nodes = []
  end

  def describe
    @nodes.collect{ |node| node.describe }.join("\n")
  end

  class Node
    attr_accessor :id, :ip, :title

    def initialize( id )
      @id = id
    end

    def describe
      "#@id -> #@ip \"#@title\""
    end

    class Builder
      def process( node, dsl )
        @node = node
        instance_eval &dsl
      end

      def ip( value )
        @node.ip = value
      end

      def title( value )
        @node.title = value
      end
    end
  end

  class Builder
    def process( manager, dsl )
      @manager = manager
      @node_builder = Node::Builder.new
      instance_eval &dsl
    end

    def node( node_id, &block )
      node = Node.new( node_id )
      @node_builder.process( node, block )
      @manager.nodes << node
    end
  end
end

dsl = lambda{
  node :node1 do
    ip '192.whatever'
    title 'Node 1'
  end

  node :node2 do
    ip '192.whatever'
    title 'Node 2'
  end
}

manager = ClusterManager.new
builder = ClusterManager::Builder.new
builder.process( manager, dsl )

puts manager.describe

###############

A few caveats about the above:

1) You'll notice I changed the DSL a little (quoted the 192.whatever
values). That was simply for brevity in this illustration.

2) My Builders require the argument to process be lambdas (Procs) not
strings. This was because I didn't want to complicate things with
switches on whether to use the prefix & or not. But it would be as
simple as an if/else to hide the lambda/string distinction from the
"user" (which will be yourself, not the person writing in the DSL)
inside the Builder. Alternatively, you can take a string and build it
into a lambda for the Builder via eval("lambda{ #{dsl} }"). I wouldn't
necessarily recommend that though (with all the evils of eval).

Jacob Fugal