Capistrano HowTo

Arti uses Capistrano to access script/console and other interactive programs on arbitrary hosts. This page provides setup instructions and discusses, in general, how the access is accomplished.

Setup

Authentication

In order to function, Capistrano needs to be able to SSH into at least one target machine. For convenience and security, it would be nice to do so without the need for passwords. So, you'll need to set up the ~/.ssh directory to allow password-free login. The simplest case to set up is localhost, but the procedure is basically the same for remote hosts.

Take a look to see whether you have a suitable public key on the "base" machine (where Capistrano will be running). This will generally be located in the ~/.ssh directory in a file named id_dsa.pub or id_rsa.pub. If not, you'll have to run ssh-keygen:

$ ssh-keygen -N 'im in ur app, doin ur docs'
Generating public/private rsa key pair.
...

Once you have a suitable key on the base machine, copy it onto the end of authorized_keys on the target machine(s). Of course, for localhost, these are the same machine:

$ cd ~/.ssh
$ more id_?sa.pub
...
$ vi authorized_keys

You should now be able to log in:

$ ssh localhost
Last login: Fri May 30 17:46:22 2008 from localhost
...

Use

Arti uses "solo" Capistrano apps, as described in "Deploying Rails Applications". A shell command executes cap on a self-contained configuration file, specifying a particular Capistrano task:

$ cap -f ./tasks.cap some_task

The task runs a command on a target machine (eg, localhost), capturing the standard error and output streams and the fact of a non-zero return code. This mechanism is simple but powerful: it allows Arti to run a wide range of interactive applications, including script/console, on any accessible machine.

Run a test command

To test this mechanism, we use a small command (x1_s) that writes to both standard error and standard output, then exits with a non-zero (1) return code:

 
$ cat x1_s
#!/usr/bin/env ruby
#
# x1_s - sub-command for use by x1.cap

       puts 'Hi on STDOUT'
STDERR.puts 'Hi on STDERR'
exit 1


$ x1_s
Hi on STDOUT
Hi on STDERR
$ echo $?
1

Run a test command via Capistrano

The Capistrono task definition file (x1.cap) is definitely more complex than the test script, but still relatively straightforward. After setting up some variables, it defines the test_cmd task. This task goes to the Work/Cap directory, runs the ./x1_s command, identifies and copies the output streams, and responds correctly to the non-zero return code:

 
$ cat x1.cap
# x1.cap

  set  :application, 'localhost'
  role :arti,        application

  desc 'Run a test command.'
  task :test_cmd, :roles => [:arti] do
    begin
      cmd = 'cd Work/Cap; ./x1_s'
      run cmd do |channel, stream, data|
        Capistrano::Configuration.
          default_io_proc.call(channel, stream, data)
      end
    rescue => e
      puts "Command failed: #{e}"
    end
  end


$ cap -f ./x1.cap test_cmd
  * executing `test_cmd'
  * executing "cd Work/Cap; ./x1_s"
    servers: ["localhost"]
    [localhost] executing command
*** [err :: localhost] tcsh: /dev/tty: Device not configured.
*** [err :: localhost] Hi on STDERR
*** [err :: localhost] 
 ** [out :: localhost] Hi on STDOUT
    command finished
Command failed: command "cd Work/Cap; ./x1_s" failed on localhost

To summarize, we were able to run a "remote" command, capture and separate its standard error and output, and detect the presence of a non-zero return code.

Run script/console via Capistrano

script/console is a powerful tool for introspection of Rails apps, but it was designed to be used interactively. Fortunately, it can also accept a stream of commands from standard input. By putting our commands into a file on the target machine (eg, /tmp/foo.rb), we can get script/console to run a sequence of commands and then quit:

$ cat /tmp/foo.rb
a = 'a test'
puts "This is #{a}."


$ ruby /tmp/foo.rb
This is a test.

We can now ask script/console to run this code. As it happens, I have a Rails tree in directory Work/STEMS/stems_t1 on machine spot.cfcl.com, so that's where we'll run script/console. Also note that we now collect and display the actual return code:

 
$ cat x2.cap
# x2.cap

  set  :application, 'spot.cfcl.com'
  role :arti,        application

  desc 'Run a test command.'
  task :test_cmd, :roles => [:arti] do
    cmds = [ 'cd Work/STEMS/stems_t1',
             'script/console < /tmp/foo.rb',
             'echo cmd_return_code: $?'
           ].join('; ')

    return_code = nil
    run cmds do |channel, stream, data|
      return_code = $1.to_i if data =~ /cmd_return_code: (\d+)/

      Capistrano::Configuration.
        default_io_proc.call(channel, stream, data)
    end

    puts "Command exited with return code #{return_code}."
  end


$ cap -f ./x2.cap test_cmd
  * executing `test_cmd'
  * executing "cd Work/STEMS/stems_t1; script/console < /tmp/foo.rb; echo return_code: $?"
    servers: ["spot.cfcl.com"]
    [spot.cfcl.com] executing command
 ** [out :: spot.cfcl.com] >> a = 'a test'
 ** [out :: spot.cfcl.com] => "a test"
 ** [out :: spot.cfcl.com] >>
 ** [out :: spot.cfcl.com] puts "This is #{a}."
 ** [out :: spot.cfcl.com] This is a test.
 ** [out :: spot.cfcl.com] => nil
 ** [out :: spot.cfcl.com] >>
 ** [out :: spot.cfcl.com] return_code: 0
    command finished
Command exited with return code 0.

In most cases, this output would be sent to a log and/or used for debugging. Given that /tmp/foo.rb can contain arbitrary Ruby code, it can easily write structured output (eg, JSON, YAML, XML) to a temporary file for Capistrano to pick up.

Run script/console from Ruby via Capistrano

Capistrano can also be run from inside a Ruby program. Conveniently, it's considerably less chatty in this mode. Even more conveniently, Capistrano's load method can be run as many times as needed, using a variety of interfaces:

 
# From .../gems/capistrano-2.3.0/lib/
#        capistrano/configuration/loading.rb
...
# Load a configuration file or string into this configuration.
#   
# Usage:
# 
#   load("recipe"):
#     Look for and load the contents of 'recipe.rb' into this
#     configuration.
# 
#   load(:file => "recipe"):
#     same as above
#
#   load(:string => "set :scm, :subversion"):
#     Load the given string as a configuration specification.
#
#   load { ... }
#     Load the block in the context of the configuration.

def load(*args, &block)
...

Here's our same example configuration file (./x2.cap), preceded and followed by comment lines:

 
$ cat x3.rb
# x3.rb

require 'rubygems'
require 'capistrano/configuration'

def do_cap(file, task)
  cc = Capistrano::Configuration.new
  cc.load(:string => '# Comment 1')
  cc.load(file)                     # -f 
  cc.load(:string => '# Comment 2')
  cc.logger.level = 1               # -v
  cc.find_and_execute_task(task)    # 
end

do_cap('./x2.cap', 'test_cmd')


$ ruby x3.rb
 ** [out :: spot.cfcl.com] >> a = 'a test'
 ** [out :: spot.cfcl.com] => "a test"
 ** [out :: spot.cfcl.com] >> puts "This is #{a}."
 ** [out :: spot.cfcl.com] This is a test.
 ** [out :: spot.cfcl.com] => nil
 ** [out :: spot.cfcl.com] >>
 ** [out :: spot.cfcl.com] cmd_return_code: 0
Command exited with return code 0.

Caveats

The net-ssh-2.0.1 gem has a bug which causes it to crash if ~/.ssh/known_hosts contains a blank line. After whinging a lot to the Capistrano mailing list, I spent a Saturday afternoon tracking down the problem. Rah! FYI, the nastygram looks like this:

Command failed: connection failed for: localhost 
  (NoMethodError: private method `split' called for nil:NilClass) 

Resources

  • Capistrano <capistrano@googlegroups.com> (mailing list)

  • #capistrano (IRC channel)

Books

    • Part XIII: Deployment and Capistrano Recipes
    • Snack Recipe 79: Respond to Remote Capistrano Prompts

    • Chapter 5: Capistrano

    • Chapter 21: Capistrano

Note: Thanks to the folks on the Capistrano mailing list and IRC channel for their help and patience. Special thanks to EzraZ, HenryA, JamisB, and ShawnB!


This wiki page is maintained by Rich Morin, an independent consultant specializing in software design, development, and documentation. Please feel free to email comments, inquiries, suggestions, etc!

Topic revision: r13 - 20 Jul 2008, RichMorin
This site is powered by Foswiki Copyright © by the contributing authors. All material on this wiki is the property of the contributing authors.
Foswiki version v2.1.6, Release Foswiki-2.1.6, Plugin API version 2.4
Ideas, requests, problems regarding CFCL Wiki? Send us email