Using Capistrano to check for deployment dependancies
January 12th, 2007
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.
January 19th, 2007 at 10:02 PM
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 :)
January 21st, 2007 at 10:18 AM
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).
January 22nd, 2007 at 08:52 PM
your task rescue me from big trouble! ...very nice job
January 23rd, 2007 at 05:49 PM
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 endI used the capistrano logger to display the output, and also display which server it relates to.
January 23rd, 2007 at 06:44 PM
@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 :)
February 3rd, 2007 at 02:41 AM
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
February 14th, 2007 at 01:47 PM
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.
May 4th, 2007 at 10:41 PM
Hi Nathan, that’s some good stuff. Any news on the plugin or gem, though? Thanks :)
August 27th, 2007 at 01:53 AM
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 :)
September 10th, 2007 at 05:58 PM
@ 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 :).
October 17th, 2007 at 05:29 PM
Thanks. Good stuff