Ruby Scratch Pad

Russell Bateman
September 2013
last update:

Ruby notes
Ruby training

This is a scratch pad for Ruby stuff I'm trying out, want to remember how to do, etc.


Ruby quick-install

# apt-get install ruby1.9.1-full
# update-alternatives --config ruby
# apt-get install build-essential
# gem install rdoc-data
# rdoc-data --install

Which is to say that it can happen and I don't yet know how to fix it.


Try out using JSON input

This demonstrates blindly trusting that I could get JSON input working without knowing anything about how to set it up. After Googling for "ruby json input", I found stackoverflow.com: how to use json as input and clicked on the link in the answer. With an uncharacteristic lack of caution, I decided to go for it haphazardly and was richly rewarded.

Also, for converting names into symbols below, see what is the best way to convert a json [...] to ruby hash with symbol as key".

In this trial are the following statements, originally sequential in irb, that demonstrate...

  1. Using Gem to get a new library, launching irb and requiring the new library.
    ~ $ sudo gem install json
    Building native extensions.  This could take a while...
    Successfully installed json-1.8.0
    1 gem installed
    Installing ri documentation for json-1.8.0...
    Installing RDoc documentation for json-1.8.0...
    ~ $ irb
    irb(main):001:0> require 'json'
    => true
    
  2. Using the library to define a Ruby variable from (ad hoc) JSON input.
    irb(main):002:0> our_planet = '{ "earth" : true, "moon" : false }'
    => "{ \"earth\" : true, \"moon\" : false }"
    
  3. Printing out the result of the assignment.
    irb(main):003:0> puts our_planet
    { "earth" : true, "moon" : false }
    => nil
    
  4. Demonstrating parsing of the JSON and that usual, quoted identifiers in JSON become (formally) symbols in Ruby if the parse option is included.
    irb(main):004:0> chez_nous = JSON.parse( our_planet, :symbolize_names => true )
    => {:earth=>true, :moon=>false}
    irb(main):005:0> puts chez_nous[ :earth ]
    true
    => nil
    irb(main):006:0> puts chez_nous[ :moon ]
    false
    => nil
    
  5. Demonstrating via conditional expression that the assignment worked and was meaningful (and that we're not being tricked by some Ruby side-effect).
    irb(main):007:0> if chez_nous[ :earth ] ; puts "It's true" ; end
    It's true
    => nil
    irb(main):008:0> if not chez_nous[ :earth ] ; puts "It's false" ; end
    => nil
    irb(main):009:0> if chez_nous[ :moon ] ; puts "It's false" ; end
    => nil
    irb(main):010:0> if not chez_nous[ :moon ] ; puts "It's false" ; end
    It's false
    => nil
    
  6. Do some other stuff worth observing in understanding variable behavior in Ruby.
    irb(main):011:0> puts our_planet
    { "earth" : true, "moon" : false }
    => nil
    irb(main):012:0> puts our_planet[ :earth ]
    TypeError: can't convert Symbol into Integer
    	from (irb):19:in `[]'
    	from (irb):19
    	from /usr/bin/irb:12:in `
    ' irb(main):013:0> puts our_planet[ "earth" ] earth => nil irb(main):014:0> puts our_planet[ "moon" ] moon => nil

Java to Ruby

I admit it, I'm not a Ruby guy. It's like when you're a hammer, everything you think you want to do looks like a nail to you no matter what it really is. In my case, all the sample code dealing with classes in Ruby was so simple and so "pre-initialized" as to be useless to me as I try to be more Ruby-esque and less Java-esque.

Turning to a question I posted on stackoverflow.com, here is what I mean. Suppose the following Java code and sample invocation thereof:

class Node
{
   private Map< String, String > normal = new HashMap< String, String >();

   public void addToNormal( String key, String value ) { this.normal.put( key, value ); }
   public String getNormal( String key )               { return this.normal.get( key ); }
}
...
Node node = new Node();
node.addToNormal( "city", "New Orleans" );
System.out.println( node.getNormal( "city" );

I want a class to imitate Chef's Node, whose definition simply isn't given, but it's the basis for what a node's doing when chef-client is run. Mostly, I want to experiment with it in Ruby so that I can write more correct code in my Chef recipe rather than put crap into the recipe that errors out with mysterious errors.

Here's what my new friend on stackoverflow gave me as a stepping off point:

class Node
  @normal = {}

  def initialize
    @normal = {}
  end

  def get_normal( n )
    @normal[ :n ]
  end

  def add_normal( n, value )
    @normal[ :n ] = value
  end
end
...
node = Node.new
node.add_normal( :city, "New Orleans" )
puts node.get_normal( :city )

The code for both these examples prints "New Orleans" to the console, here's Ruby:

~ $ irb
irb(main):001:0> class Node
irb(main):002:1>   @normal = {}
irb(main):003:1>
irb(main):004:1*   def initialize
irb(main):005:2>     @normal = {}
irb(main):006:2>   end
irb(main):007:1>
irb(main):008:1*   def get_normal( n )
irb(main):009:2>     @normal[ :n ]
irb(main):010:2>   end
irb(main):011:1>
irb(main):012:1*   def add_normal( n, value )
irb(main):013:2>     @normal[ :n ] = value
irb(main):014:2>   end
irb(main):015:1> end
=> nil
irb(main):016:0> node = Node.new
=> #<Node:0x00000001edd270 @normal={}>
irb(main):017:0> node.add_normal( :city, "New Orleans" )
=> "New Orleans"
irb(main):018:0> puts node.get_normal( :city )
New Orleans
=> nil

Still, this is wrong-headed. I really need to replicate how node in Chef works. So, along those lines, here's a pretty thing:


Ruby nested hash initialization and access...

Finally. Ruby is not taking her love to town on me every time. Observe this code. The first line is the definition of node's normal field in the Chef node. No, I don't know what it means yet. The subsequent lines imitate what happens when Chef reads in attributes from attribute or node files:

normal = Hash.new { | h, k | h[ k ] = Hash.new( &h.default_proc ) }

normal[ :mongodb ][ :package ]                = "mongodb-10gen"
normal[ :mongodb ][ :replicaset ][ :name ]    = "myreplicas"
normal[ :mongodb ][ :replica_1 ][ :hostname ] = "16.86.193.101"
normal[ :mongodb ][ :replica_1 ][ :port ]     = 37017
normal[ :mongodb ][ :replica_2 ][ :hostname ] = "16.86.193.102"
normal[ :mongodb ][ :replica_2 ][ :port ]     = 37018
normal[ :mongodb ][ :replica_3 ][ :hostname ] = "16.86.193.103"
normal[ :mongodb ][ :replica_3 ][ :port ]     = 37019

...and its behavior, answers in bold. This is just the stuff I need to ferret out in my MongoDB recipe that's going to execute commands in the MongoDB shell when it erects a replica set. Ruby's unless keyword is just what I need too:

irb(main):010:0> puts normal[ :mongodb ][ :package ]
mongodb-10gen
=> nil
irb(main):011:0> puts normal[ :mongodb ][ :replicaset ][ :name ]
myreplicas
=> nil
irb(main):012:0> puts normal[ :mongodb ][ :replica_1 ][ :hostname ]
16.86.193.101
=> nil
irb(main):013:0> puts normal[ :mongodb ][ :replica_1 ][ :port ]
37017
=> nil
irb(main):014:0> puts normal[ :mongodb ][ :replica_2 ][ :hostname ]
16.86.193.102
=> nil
irb(main):015:0> puts normal[ :mongodb ][ :replica_2 ][ :port ]
37018
=> nil
irb(main):016:0> puts normal[ :mongodb ][ :replica_3 ][ :hostname ]
16.86.193.103
=> nil
irb(main):017:0> puts normal[ :mongodb ][ :replica_3 ][ :port ]
37019
=> nil
irb(main):018:0> unless normal[ :mongodb ][ :replica_4 ]
irb(main):019:1>   puts "I did it."
irb(main):020:1> else
irb(main):021:1*   puts "I didn't do it."
irb(main):022:1> end
I didn't do it.
=> nil

Yeah, there is no "replica_4", that's the point. By the way, here's what this looks like in the JSON node file in Chef:

{
    "normal":
    {
        "mongodb" :
        {
          "replicanode" : { "hostname" : 16.86.193.100 },
          "replicanode" : { "port" : 37017 }
        }
        "replica_1" :
        {
          "hostname" : 16.86.193.100,
          "port"     : 37017
        },
        "replica_2" :
        {
          "hostname" : 16.86.193.101,
          "port"     : 37018
        },
        "replica_3" :
        {
          "hostname" : 16.86.193.102,
          "port"     : 37019
        },
        "arbiter_1" :
        {
          "hostname" : 16.86.193.103,
          "port"     : 37016
        }
    },
    "name": "db01",
    "override": { },
    "default": { },
    "json_class": "Chef::Node",
    "automatic": { },
    "run_list":
    [
        "recipe[apt]",
        "recipe[mongodb]",
        "recipe[mongodb::replica]",
        "recipe[mongodb::post-replicas]",
        "role[install_database_node]",
        "role[install_replica_node]",
        "role[install_post_replica_node]"
    ],
    "chef_type": "node"
}

A working Chefish example...

I'm writing a Chef recipe getting ready to issue commands to the MongoDB shell. I'm constructing a command to configure a replica set based on recorded information from JSON in a node file.

# --------------------------------------------------------------------------+
# This simulates 'node' as Chef will set it up in chef-client run.        # |
    node = Hash.new { | h, k | h[ k ] = Hash.new( &h.default_proc ) }     # |
                                                                          # |
    # Contributed by attributes/default.rb.                               # |
    node[ :mongodb ][ :package ]                  = "mongodb-10gen"       # |
                                                                          # |
    # Contributed by nodes/db01.json, assuming using it as the "tie-up"   # |
    # node that's going to be run after all others are set up.            # |
    node[ :mongodb ][ :replicanode ][ :hostname ] = "16.86.193.100"       # |
    node[ :mongodb ][ :replicanode ][ :port ]     = 37016                 # |
    node[ :mongodb ][ :replicaset ][ :name ]      = "myreplicas"          # |
    node[ :replica_1 ] [ :hostname ]              = "16.86.193.100"       # |
    node[ :replica_1 ] [ :port ]                  = 37017                 # |
    node[ :replica_2 ] [ :hostname ]              = "16.86.193.101"       # |
    node[ :replica_2 ] [ :port ]                  = 37018                 # |
    node[ :replica_3 ] [ :hostname ]              = "16.86.193.102"       # |
    node[ :replica_3 ] [ :port ]                  = 37019                 # |
    node[ :arbiter_1 ] [ :hostname ]              = "16.86.193.103"       # |
    node[ :arbiter_1 ] [ :port ]                  = 37016                 # |
# --------------------------------------------------------------------------+

id = 0
found = false
replica_members = ""

if !node[ :replica_1 ].empty?
   found = true
   hostname = node[ :replica_1 ][ :hostname ]
   port     = node[ :replica_1 ][ :port ]
   replica_members += "{ _id:%d, host:%s:%s }" % [ id, hostname, port ]
   id += 1
end
if !node[ :replica_2 ].empty?
   if found
     replica_members += ", "
   end
   found = true
   hostname = node[ :replica_2 ][ :hostname ]
   port     = node[ :replica_2 ][ :port ]
   replica_members += "{ _id:%d, host:%s:%s }" % [ id, hostname, port ]
   id += 1
end
if !node[ :replica_3 ].empty?
   if found
     replica_members += ", "
   end
   found = true
   hostname = node[ :replica_3 ][ :hostname ]
   port     = node[ :replica_3 ][ :port ]
   replica_members += "{ _id:%d, host:%s:%s }" % [ id, hostname, port ]
   id += 1
end
if !node[ :replica_4 ].empty?
   if found
     replica_members += ", "
   end
   hostname = node[ :replica_4 ][ :hostname ]
   port     = node[ :replica_4 ][ :port ]
   replica_members += "{ _id:%d, host:%s:%s }" % [ id, hostname, port ]
   id += 1
end

replicaset_name = node[ :mongodb ][ :replicaset ][ :name ]
replica_id      = "id : \"%s\"" % replicaset_name
configuration   = "config = { " + replica_id + ", members: [" + replica_members + "] }"

# Print out what has happened...
print "replicaset_name = ", replicaset_name
puts
print "     replica_id = ", replica_id
puts
print "replica_members = ", replica_members
puts
puts "  configuration = ", configuration

And the console output appears thus, though I've re-formatted it a) to put it on multiple lines and b) to demonstrate its JSON correctness (MongoDB uses JSON as its shell language). I added color and text face too.

replicaset_name = myreplicas
     replica_id = id : "myreplicas"
replica_members = { _id:0, host:16.86.193.100:37017 },
                         { _id:1, host:16.86.193.101:37018 },
                         { _id:2, host:16.86.193.102:37019 }
  configuration = config =
                  {
                      id : "myreplicas",
                      members:
                      [
                          { _id:0, host:16.86.193.100:37017 },
                          { _id:1, host:16.86.193.101:37018 },
                          { _id:2, host:16.86.193.102:37019 }
                      ]
                  }