Problem: You’d like to deploy an application using Capistrano but you’re not sure if the remote machine has all the dependencies.

Solution: Write a Capistrano task which checks for required binaries (and versions) and reports on any discrepancies

Sounds simple enough, right? Here’s how I’ve done it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# This class is stored separately & included, of course. It'll all work in the same file for now, though.
class VersionError < Exception
  attr :required_version
  attr :installed_version
  
  def initialize(required,installed)
    @required_version = required
    @installed_version = installed
  end
end

role :app, "localhost"

# Formatted as package name, friendly version, command to execute, successful regex, version regex
set :dependancies, [
  ['ruby', '1.8.4 or 1.8.5', 'ruby -v', /ruby (1\.8\.[4-5])/, /\d\.\d\.\d/],
  ['rake', '0.x.x', 'rake --version', /rake, version (\d\.\d\.\d)/, /\d\.\d\.\d/],
  ['python', '2.4.x', 'python -V', /Python (2\.4\.\d)/, /\d\.\d\.\d/],
  ['pyro', '3.x', 'python -c "import Pyro.core; print Pyro.core.constants.VERSION"', /3\.\d/, /\d\.\d/],
  ['pil', '1.1.x', 'python -c "import Image; print Image.VERSION"', /1\.1\.\d/, /\d\.\d\.\d/],
  ['mysql', '5.0.x', 'mysql -V', /Distrib (5\.0\.\d+)/, /\d\.\d\.\d+/],
  ['lighttpd', '1.4.x', 'lighttpd -v', /lighttpd-(1\.4\.\d+)/, /\d\.\d\.\d+/],
]

dependancies.each do |dep|
  task "#{dep.first}_installed?".to_sym do
    begin
      run dep[2] do |channel,stream,data|
        next if data =~ /command not found/
        begin
          if not data =~ dep[3]
            if data =~ dep[4]
              raise VersionError.new(dep[1], $~[0])
            else
              raise VersionError.new(dep[1], 'undetermined version')
            end
          else
            puts "#{dep[0].upcase} (#{dep[1]}) is installed correctly."
          end
        rescue VersionError => v
          puts "#{dep[0].upcase} version incorrect! #{v.required_version} required, but found #{v.installed_version}"
        end
      end
    rescue RuntimeError
      puts "#{dep[0].upcase} is not installed (or isn't in your path). Please install version #{dep[1]}."
    end
  end
end

desc "Check if the machine is ready to be taken into the fold."
task :ready? do
  dependancies.each do |dep|
    eval("#{dep.first}_installed?")
  end
end

Now, all I need to do is run the “ready?” task, and I’ll be able to tell if I need to install/upgrade any applications on the remote host(s). Like so:

$ cap ready?

SUBVERSION is not installed (or isn't in your path). Please install version 1.3.x or higher.
RUBY is not installed (or isn't in your path). Please install version 1.8.4 or 1.8.5.
RAKE is not installed (or isn't in your path). Please install version 0.x.x.
PYTHON (2.4.x) is installed correctly.
PYTHON_PYRO is not installed (or isn't in your path). Please install version 3.x.
PYTHON_PIL is not installed (or isn't in your path). Please install version 1.1.x.
PYTHON_MYSQLDB is not installed (or isn't in your path). Please install version any.
MYSQL is not installed (or isn't in your path). Please install version 5.0.x.
LIGHTTPD is not installed (or isn't in your path). Please install version 1.4.x.

At the moment, this works perfectly fine for me…but does anyone have an idea how it could be improved? At the moment I’ve just hacked around the issue, but I’m sure something a little more generic (using “expected output” and “actual output” instead of versions) could be included within Capistrano itself.

11 Responses to “Using Capistrano to check for deployment dependancies”

  1. Dr Nic Says:

    It’d be cool to check availability of required gems too. This was something I wanted to do with the gemsonrails plugin, but its still on the todo list :)

  2. Nathan de Vries Says:

    Checking for gem dependencies wouldn’t require any changes to the code…you’d just need to add an element to your :dependencies array which runs `gem list—local <gemname>`. You would have one of these for each gem you require on the remote machine(s).

  3. Michele Says:

    your task rescue me from big trouble! ...very nice job

  4. Evan Says:

    Here’s my suggestion for improving your script:

    dependancies.each do |dep| task "#{dep.first}_installed?".to_sym do begin run dep[2] do |channel,stream,data| next if data =~ /command not found/ begin if not data =~ dep[3] if data =~ dep[4] raise VersionError.new(dep[1], $~[0]) else raise VersionError.new(dep[1], 'undetermined version') end else logger.info "#{dep[0].upcase} (#{dep[1]}) is installed correctly.", channel[:host] end rescue VersionError => v logger.important "#{dep[0].upcase} version incorrect! #{v.required_version} required, but found #{v.installed_version}", channel[:host] end end rescue RuntimeError logger.important "#{dep[0].upcase} is not installed (or isn't in your path). Please install version #{dep[1]}.", channel[:host] end end end

    I used the capistrano logger to display the output, and also display which server it relates to.

  5. Nathan de Vries Says:

    @Evan: Thanks for that. I hadn’t even thought of using Capistrano’s logger. I’ve just finished turning this into a Capistrano plugin, which will soon become a gem. I’ll probably implement your logger suggestion before that happens :)

  6. Jean-Michel Garnier Says:

    I am willing to work on a similar project, would you mind take a look to the specs at http://writer.zoho.com/public/garnierjm/ruby-dependency-management-specs

    I would very happy if you could contribute to the specs / code!

    Jean-Michel

  7. Nathan de Vries Says:

    Hi Jean-Michel, Unfortunately I don’t have too much time to help you out with your project (it’s a great idea). Keep your eye on my blog though, because I’ve made a couple of updates and will be releasing the Capistrano dependency checker as a gem shortly.

  8. Hendrik Says:

    Hi Nathan, that’s some good stuff. Any news on the plugin or gem, though? Thanks :)

  9. Steve Roth Says:

    Hi Nathan, I don’t have an idea for further improving but what you have done works perfectly for me, especially with Evan’s improvement. Thanks :)

  10. Nathan de Vries Says:

    @ Hendrik & Steve: Thanks for the feedback, glad you’re finding it useful. I never got around to creating a gem, mainly because just as I was close to finishing, Capistrano 2.0 was released which contains Jamis’ own dependency checking features :).

  11. Saving Silverman Says:

    Thanks. Good stuff

Leave a Reply