Using Capistrano To Check For Deployment Dependencies

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:

Check remote dependencies are met via Capistrano
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 :dependencies, [
  ['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+/],
]

dependencies.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
  dependencies.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:

Invoking the ready task via the shell
1
2
3
4
5
6
7
8
9
10
11
$ 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.