Skip to content

Instantly share code, notes, and snippets.

@perl2ruby
Created July 11, 2010 03:27
Show Gist options
  • Save perl2ruby/471253 to your computer and use it in GitHub Desktop.
Save perl2ruby/471253 to your computer and use it in GitHub Desktop.
Dynamic Capistrano:
Is a way of writing a capistrano Capfile where roles, tasks, namespaces etc. are
dynamically generated and not much is hardcoded in the file. This allows for tremendous
amount of flexibility in creating much more powerful Capfile. This also facilitates the
reuse of roles/tasks across projects (by sharing of config files), eliminating the problems
related to duplication.
[Thanks to Alain Hoang for suggestions and feedback]
Dynamic Capistrano relies on the main Capfile and a bunch of other config files which provide:
meta information for generating tasks. We have the following files involved:
- A Capfile with very little hardcoding
- A host_role mapfile indicating the roles and the hosts assigned to that role
- A task_role mapfile indicating the tasks (along with namespace) and the role they apply to
- A command file for each task. Its possible that tasks can share the same command file
Notes:
- We can extend this concept so that we have qualifiers for roles associated with a task
so that only a subset of hosts in a role are involved with a given task
- Add provision to accommodate other Capistrano directives such as MAX_HOSTS
- We can add a debug option so that only the intermediate Capistrano file is written
out without actually executing any task
The contents of the cap file are:
#---- Begin: Capfile ----------
require 'yaml'
set :default_environment, {
:PATH => '/usr/bin:/usr/local/bin'
}
# define the roles by reading a file (host_role.map)
# format of the file is:
# <hostname>:<rolename>
#
# Sample contents of the file host_role.map are:
#
# qaweb1:qaweb
# web1:web
# web2:web
# web3:web
# db1:db
# scm1:scm
#
roles = []
host_role_mapfile = "host_role.map"
# read the host_role mapfile and define the roles
File.open(host_role_mapfile, "r") do |infile|
while (line = infile.gets)
node, role = line.split(/:/)
role "#{role}".to_sym, node
roles.push("#{role}".to_sym)
end
end
# Based on the sample host_role.map,
# the above capistrano code results in the roles
# being defined as follows:
# role :qaweb, qaweb1
# role :web, web1
# role :web, web2
# role :web, web3
# role :db, db1
# role :scm, scm1
# define the tasks by reading a file (task_role.map)
# format of the file is:
# mynamespace=<namespace>:mytask=<taskname>:mygateway=<gwname>:myrole=<rolename>:mycmdfile=<cmdfile>
#
# Sample contents of the file task_role.map are:
# mynamespace=prod:mytask=restart_apache:mygateway=prodgw1:myrole=web:mycmdfile=prod_restart_apache.cmd
# mynamespace=qa:mytask=restart_db:mygateway=qagw1:myrole=qadb:mycmdfile=qa_restart_db.cmd
# mynamespace=qa:mytask=restart_apache:mygateway=qagw1:myrole=qaweb:mycmdfile=qa_restart_apache.cmd
#
# NOTE: One can specify the abs path for command files
#
elements = []
task_role_mapfile = "task_role.map"
File.open(tasks_role_mapfile, "r") do |infile|
while (line = infile.gets)
entries = line.split(/:/)
entry = { }
entries.each do |parmval|
parm, val = parmval.split(/=/)
entry[parm] = val
end
elements.push(entry)
end
end
# The above results in elements:
# elements = [
# {
# "mynamespace" => "prod",
# "mytask" => "restart_apache",
# "mygateway" => "prodgw1",
# "myrole" => "web",
# "mycmdfile" => "prod_restart_apache.cmd"
# },
# {
# "mynamespace" => "qa",
# "mytask" => "restart_db",
# "mygateway" => "qagw1",
# "myrole" => "qadb",
# "mycmdfile" => "qa_restart_db.cmd"
# },
# {
# "mynamespace" => "qa",
# "mytask" => "restart_apache",
# "mygateway" => "qagw1",
# "myrole" => "qaweb",
# "mycmdfile" => "qa_restart_apache.cmd"
# }
# ]
#
# Notice that even though both tasks are labeled restart_apache,
# they are in different namespaces
#
# The contents of the command files are:
# -- contents of qa_restart_apache.cmd ---
# sudo apachectl restart
#
# -- contents of prod_restart_apache.cmd ---
# sudo apachectl restart
#
# -- contents of qa_restart_db.cmd ---
# sudo /etc/init.d/postgresql restart
#
# we now go thru all the elements to generate the tasks
# without having to enumerate
elements.each do |element|
namespace element["mynamespace"].to_sym do
task element["mytask"].to_sym, :roles => [element["myrole"].to_sym] do
set :gateway, element["mygateway"]
cmds = []
task_cmd_mapfile = "#{element['mycmdfile']}"
File.open(task_cmd_mapfile, "r") do |infile|
while (line = infile.gets)
cmds.push(line)
end
run cmds.join(' && ')
end
end
end
end
# The tasks that get generated appear as follows:
# namespace :prod do
# task :restart_apache, :roles =>[:web] do
# set :gateway, prodgw1
# [ "sudo apachectl restart" ].join('&&')
# end
# end
# namespace :qa do
# task :restart_apache, :roles =>[:qaweb] do
# set :gateway, qagw1
# [ "sudo apachectl restart" ].join('&&')
# end
# end
# namespace :qa do
# task :restart_db, :roles =>[:qaweb] do
# set :gateway, qagw1
# [ "sudo /etc/init.d/postgresql restart" ].join('&&')
# end
# end
#---- End: Capfile ----------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment