capistrano 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/bin/cap +11 -0
  2. data/examples/sample.rb +113 -0
  3. data/lib/capistrano.rb +1 -0
  4. data/lib/capistrano/actor.rb +438 -0
  5. data/lib/capistrano/cli.rb +295 -0
  6. data/lib/capistrano/command.rb +90 -0
  7. data/lib/capistrano/configuration.rb +243 -0
  8. data/lib/capistrano/extensions.rb +38 -0
  9. data/lib/capistrano/gateway.rb +118 -0
  10. data/lib/capistrano/generators/rails/deployment/deployment_generator.rb +25 -0
  11. data/lib/capistrano/generators/rails/deployment/templates/capistrano.rake +46 -0
  12. data/lib/capistrano/generators/rails/deployment/templates/deploy.rb +122 -0
  13. data/lib/capistrano/generators/rails/loader.rb +20 -0
  14. data/lib/capistrano/logger.rb +59 -0
  15. data/lib/capistrano/recipes/standard.rb +242 -0
  16. data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
  17. data/lib/capistrano/scm/base.rb +62 -0
  18. data/lib/capistrano/scm/baz.rb +118 -0
  19. data/lib/capistrano/scm/bzr.rb +70 -0
  20. data/lib/capistrano/scm/cvs.rb +124 -0
  21. data/lib/capistrano/scm/darcs.rb +27 -0
  22. data/lib/capistrano/scm/perforce.rb +139 -0
  23. data/lib/capistrano/scm/subversion.rb +122 -0
  24. data/lib/capistrano/ssh.rb +39 -0
  25. data/lib/capistrano/transfer.rb +90 -0
  26. data/lib/capistrano/utils.rb +26 -0
  27. data/lib/capistrano/version.rb +30 -0
  28. data/test/actor_test.rb +294 -0
  29. data/test/command_test.rb +43 -0
  30. data/test/configuration_test.rb +233 -0
  31. data/test/fixtures/config.rb +5 -0
  32. data/test/fixtures/custom.rb +3 -0
  33. data/test/scm/cvs_test.rb +186 -0
  34. data/test/scm/subversion_test.rb +137 -0
  35. data/test/ssh_test.rb +104 -0
  36. data/test/utils.rb +50 -0
  37. metadata +107 -0
data/bin/cap ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'rubygems'
5
+ rescue LoadError
6
+ # no rubygems to load, so we fail silently
7
+ end
8
+
9
+ require 'capistrano/cli'
10
+
11
+ Capistrano::CLI.execute!
@@ -0,0 +1,113 @@
1
+ # You must always specify the application and repository for every recipe. The
2
+ # repository must be the URL of the repository you want this recipe to
3
+ # correspond to. The deploy_to path must be the path on each machine that will
4
+ # form the root of the application path.
5
+
6
+ set :application, "sample"
7
+ set :repository, "http://svn.example.com/#{application}/trunk"
8
+
9
+ # The deploy_to path is optional, defaulting to "/u/apps/#{application}".
10
+
11
+ set :deploy_to, "/path/to/app/root"
12
+
13
+ # The user value is optional, defaulting to user-name of the current user. This
14
+ # is the user name that will be used when logging into the deployment boxes.
15
+
16
+ set :user, "flippy"
17
+
18
+ # By default, the source control module (scm) is set to "subversion". You can
19
+ # set it to any supported scm:
20
+
21
+ set :scm, :subversion
22
+
23
+ # gateway is optional, but allows you to specify the address of a computer that
24
+ # will be used to tunnel other requests through, such as when your machines are
25
+ # all behind a VPN or something
26
+
27
+ set :gateway, "gateway.example.com"
28
+
29
+ # You can define any number of roles, each of which contains any number of
30
+ # machines. Roles might include such things as :web, or :app, or :db, defining
31
+ # what the purpose of each machine is. You can also specify options that can
32
+ # be used to single out a specific subset of boxes in a particular role, like
33
+ # :primary => true.
34
+
35
+ role :web, "www01.example.com", "www02.example.com"
36
+ role :app, "app01.example.com", "app02.example.com", "app03.example.com"
37
+ role :db, "db01.example.com", :primary => true
38
+ role :db, "db02.example.com", "db03.example.com"
39
+
40
+ # Define tasks that run on all (or only some) of the machines. You can specify
41
+ # a role (or set of roles) that each task should be executed on. You can also
42
+ # narrow the set of servers to a subset of a role by specifying options, which
43
+ # must match the options given for the servers to select (like :primary => true)
44
+
45
+ desc <<DESC
46
+ An imaginary backup task. (Execute the 'show_tasks' task to display all
47
+ available tasks.)
48
+ DESC
49
+
50
+ task :backup, :roles => :db, :only => { :primary => true } do
51
+ # the on_rollback handler is only executed if this task is executed within
52
+ # a transaction (see below), AND it or a subsequent task fails.
53
+ on_rollback { delete "/tmp/dump.sql" }
54
+
55
+ run "mysqldump -u theuser -p thedatabase > /tmp/dump.sql" do |ch, stream, out|
56
+ ch.send_data "thepassword\n" if out =~ /^Enter password:/
57
+ end
58
+ end
59
+
60
+ # Tasks may take advantage of several different helper methods to interact
61
+ # with the remote server(s). These are:
62
+ #
63
+ # * run(command, options={}, &block): execute the given command on all servers
64
+ # associated with the current task, in parallel. The block, if given, should
65
+ # accept three parameters: the communication channel, a symbol identifying the
66
+ # type of stream (:err or :out), and the data. The block is invoked for all
67
+ # output from the command, allowing you to inspect output and act
68
+ # accordingly.
69
+ # * sudo(command, options={}, &block): same as run, but it executes the command
70
+ # via sudo.
71
+ # * delete(path, options={}): deletes the given file or directory from all
72
+ # associated servers. If :recursive => true is given in the options, the
73
+ # delete uses "rm -rf" instead of "rm -f".
74
+ # * put(buffer, path, options={}): creates or overwrites a file at "path" on
75
+ # all associated servers, populating it with the contents of "buffer". You
76
+ # can specify :mode as an integer value, which will be used to set the mode
77
+ # on the file.
78
+ # * render(template, options={}) or render(options={}): renders the given
79
+ # template and returns a string. Alternatively, if the :template key is given,
80
+ # it will be treated as the contents of the template to render. Any other keys
81
+ # are treated as local variables, which are made available to the (ERb)
82
+ # template.
83
+
84
+ desc "Demonstrates the various helper methods available to recipes."
85
+ task :helper_demo do
86
+ # "setup" is a standard task which sets up the directory structure on the
87
+ # remote servers. It is a good idea to run the "setup" task at least once
88
+ # at the beginning of your app's lifetime (it is non-destructive).
89
+ setup
90
+
91
+ buffer = render("maintenance.rhtml", :deadline => ENV['UNTIL'])
92
+ put buffer, "#{shared_path}/system/maintenance.html", :mode => 0644
93
+ sudo "killall -USR1 dispatch.fcgi"
94
+ run "#{release_path}/script/spin"
95
+ delete "#{shared_path}/system/maintenance.html"
96
+ end
97
+
98
+ # You can use "transaction" to indicate that if any of the tasks within it fail,
99
+ # all should be rolled back (for each task that specifies an on_rollback
100
+ # handler).
101
+
102
+ desc "A task demonstrating the use of transactions."
103
+ task :long_deploy do
104
+ transaction do
105
+ update_code
106
+ disable_web
107
+ symlink
108
+ migrate
109
+ end
110
+
111
+ restart
112
+ enable_web
113
+ end
@@ -0,0 +1 @@
1
+ require 'capistrano/configuration'
@@ -0,0 +1,438 @@
1
+ require 'erb'
2
+ require 'capistrano/command'
3
+ require 'capistrano/transfer'
4
+ require 'capistrano/gateway'
5
+ require 'capistrano/ssh'
6
+ require 'capistrano/utils'
7
+
8
+ module Capistrano
9
+
10
+ # An Actor is the entity that actually does the work of determining which
11
+ # servers should be the target of a particular task, and of executing the
12
+ # task on each of them in parallel. An Actor is never instantiated
13
+ # directly--rather, you create a new Configuration instance, and access the
14
+ # new actor via Configuration#actor.
15
+ class Actor
16
+
17
+ # An adaptor for making the SSH interface look and act like that of the
18
+ # Gateway class.
19
+ class DefaultConnectionFactory #:nodoc:
20
+ def initialize(config)
21
+ @config= config
22
+ end
23
+
24
+ def connect_to(server)
25
+ SSH.connect(server, @config)
26
+ end
27
+ end
28
+
29
+ class <<self
30
+ attr_accessor :connection_factory
31
+ attr_accessor :command_factory
32
+ attr_accessor :transfer_factory
33
+ attr_accessor :default_io_proc
34
+ end
35
+
36
+ self.connection_factory = DefaultConnectionFactory
37
+ self.command_factory = Command
38
+ self.transfer_factory = Transfer
39
+
40
+ self.default_io_proc = Proc.new do |ch, stream, out|
41
+ level = out == :error ? :important : :info
42
+ ch[:actor].logger.send(level, out, "#{stream} :: #{ch[:host]}")
43
+ end
44
+
45
+ # The configuration instance associated with this actor.
46
+ attr_reader :configuration
47
+
48
+ # A hash of the tasks known to this actor, keyed by name. The values are
49
+ # instances of Actor::Task.
50
+ attr_reader :tasks
51
+
52
+ # A hash of the SSH sessions that are currently open and available.
53
+ # Because sessions are constructed lazily, this will only contain
54
+ # connections to those servers that have been the targets of one or more
55
+ # executed tasks.
56
+ attr_reader :sessions
57
+
58
+ # The call stack of the tasks. The currently executing task may inspect
59
+ # this to see who its caller was. The current task is always the last
60
+ # element of this stack.
61
+ attr_reader :task_call_frames
62
+
63
+ # The history of executed tasks. This will be an array of all tasks that
64
+ # have been executed, in the order in which they were called.
65
+ attr_reader :task_call_history
66
+
67
+ # A struct for representing a single instance of an invoked task.
68
+ TaskCallFrame = Struct.new(:name, :rollback)
69
+
70
+ # Represents the definition of a single task.
71
+ class Task #:nodoc:
72
+ attr_reader :name, :actor, :options
73
+
74
+ def initialize(name, actor, options)
75
+ @name, @actor, @options = name, actor, options
76
+ @servers = nil
77
+ end
78
+
79
+ # Returns the list of servers (_not_ connections to servers) that are
80
+ # the target of this task.
81
+ def servers
82
+ unless @servers
83
+ roles = [*(@options[:roles] || actor.configuration.roles.keys)].
84
+ map { |name|
85
+ actor.configuration.roles[name] or
86
+ raise ArgumentError, "task #{self.name.inspect} references non-existant role #{name.inspect}"
87
+ }.flatten
88
+ only = @options[:only] || {}
89
+
90
+ unless only.empty?
91
+ roles = roles.delete_if do |role|
92
+ catch(:done) do
93
+ only.keys.each do |key|
94
+ throw(:done, true) if role.options[key] != only[key]
95
+ end
96
+ false
97
+ end
98
+ end
99
+ end
100
+
101
+ @servers = roles.map { |role| role.host }.uniq
102
+ end
103
+
104
+ @servers
105
+ end
106
+ end
107
+
108
+ def initialize(config) #:nodoc:
109
+ @configuration = config
110
+ @tasks = {}
111
+ @task_call_frames = []
112
+ @sessions = {}
113
+ @factory = self.class.connection_factory.new(configuration)
114
+ end
115
+
116
+ # Define a new task for this actor. The block will be invoked when this
117
+ # task is called.
118
+ def define_task(name, options={}, &block)
119
+ @tasks[name] = (options[:task_class] || Task).new(name, self, options)
120
+ define_method(name) do
121
+ send "before_#{name}" if respond_to? "before_#{name}"
122
+ logger.debug "executing task #{name}"
123
+ begin
124
+ push_task_call_frame name
125
+ result = instance_eval(&block)
126
+ ensure
127
+ pop_task_call_frame
128
+ end
129
+ send "after_#{name}" if respond_to? "after_#{name}"
130
+ result
131
+ end
132
+ end
133
+
134
+ # Iterates over each task, in alphabetical order. A hash object is
135
+ # yielded for each task, which includes the task's name (:name), the
136
+ # length of the longest task name (:longest), and the task's description,
137
+ # reformatted as a single line (:desc).
138
+ def each_task
139
+ keys = tasks.keys.sort_by { |a| a.to_s }
140
+ longest = keys.inject(0) { |len,key| key.to_s.length > len ? key.to_s.length : len } + 2
141
+
142
+ keys.sort_by { |a| a.to_s }.each do |key|
143
+ desc = (tasks[key].options[:desc] || "").gsub(/(?:\r?\n)+[ \t]*/, " ").strip
144
+ info = { :task => key, :longest => longest, :desc => desc }
145
+ yield info
146
+ end
147
+ end
148
+
149
+ # Dump all tasks and (brief) descriptions in YAML format for consumption
150
+ # by other processes. Returns a string containing the YAML-formatted data.
151
+ def dump_tasks
152
+ data = ""
153
+ each_task do |info|
154
+ desc = info[:desc].split(/\. /).first || ""
155
+ desc << "." if !desc.empty? && desc[-1] != ?.
156
+ data << "#{info[:task]}: #{desc}\n"
157
+ end
158
+ data
159
+ end
160
+
161
+ # Execute the given command on all servers that are the target of the
162
+ # current task. If a block is given, it is invoked for all output
163
+ # generated by the command, and should accept three parameters: the SSH
164
+ # channel (which may be used to send data back to the remote process),
165
+ # the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
166
+ # stdout), and the data that was received.
167
+ #
168
+ # If +pretend+ mode is active, this does nothing.
169
+ def run(cmd, options={}, &block)
170
+ block ||= default_io_proc
171
+ logger.debug "executing #{cmd.strip.inspect}"
172
+
173
+ execute_on_servers(options) do |servers|
174
+ # execute the command on each server in parallel
175
+ command = self.class.command_factory.new(servers, cmd, block, options, self)
176
+ command.process! # raises an exception if command fails on any server
177
+ end
178
+ end
179
+
180
+ # Deletes the given file from all servers targetted by the current task.
181
+ # If <tt>:recursive => true</tt> is specified, it may be used to remove
182
+ # directories.
183
+ def delete(path, options={})
184
+ cmd = "rm -%sf #{path}" % (options[:recursive] ? "r" : "")
185
+ run(cmd, options)
186
+ end
187
+
188
+ # Store the given data at the given location on all servers targetted by
189
+ # the current task. If <tt>:mode</tt> is specified it is used to set the
190
+ # mode on the file.
191
+ def put(data, path, options={})
192
+ if Capistrano::SFTP
193
+ execute_on_servers(options) do |servers|
194
+ transfer = self.class.transfer_factory.new(servers, self, path, :data => data,
195
+ :mode => options[:mode])
196
+ transfer.process!
197
+ end
198
+ else
199
+ # Poor-man's SFTP... just run a cat on the remote end, and send data
200
+ # to it.
201
+
202
+ cmd = "cat > #{path}"
203
+ cmd << " && chmod #{options[:mode].to_s(8)} #{path}" if options[:mode]
204
+ run(cmd, options.merge(:data => data + "\n\4")) do |ch, stream, out|
205
+ logger.important out, "#{stream} :: #{ch[:host]}" if stream == :err
206
+ end
207
+ end
208
+ end
209
+
210
+ # Like #run, but executes the command via <tt>sudo</tt>. This assumes that
211
+ # the sudo password (if required) is the same as the password for logging
212
+ # in to the server.
213
+ def sudo(command, options={}, &block)
214
+ block ||= default_io_proc
215
+
216
+ # in order to prevent _each host_ from prompting when the password was
217
+ # wrong, let's track which host prompted first and only allow subsequent
218
+ # prompts from that host.
219
+ prompt_host = nil
220
+
221
+ run "sudo #{command}", options do |ch, stream, out|
222
+ if out =~ /^Password:/
223
+ ch.send_data "#{password}\n"
224
+ elsif out =~ /try again/
225
+ if prompt_host.nil? || prompt_host == ch[:host]
226
+ prompt_host = ch[:host]
227
+ logger.important out, "#{stream} :: #{ch[:host]}"
228
+ # reset the password to it's original value and prepare for another
229
+ # pass (the reset allows the password prompt to be attempted again
230
+ # if the password variable was originally a proc (the default)
231
+ set :password, self[:original_value][:password] || self[:password]
232
+ end
233
+ else
234
+ block.call(ch, stream, out)
235
+ end
236
+ end
237
+ end
238
+
239
+ # Renders an ERb template and returns the result. This is useful for
240
+ # dynamically building documents to store on the remote servers.
241
+ #
242
+ # Usage:
243
+ #
244
+ # render("something", :foo => "hello")
245
+ # look for "something.rhtml" in the current directory, or in the
246
+ # capistrano/recipes/templates directory, and render it with
247
+ # foo defined as a local variable with the value "hello".
248
+ #
249
+ # render(:file => "something", :foo => "hello")
250
+ # same as above
251
+ #
252
+ # render(:template => "<%= foo %> world", :foo => "hello")
253
+ # treat the given string as an ERb template and render it with
254
+ # the given hash of local variables active.
255
+ def render(*args)
256
+ options = args.last.is_a?(Hash) ? args.pop : {}
257
+ options[:file] = args.shift if args.first.is_a?(String)
258
+ raise ArgumentError, "too many parameters" unless args.empty?
259
+
260
+ case
261
+ when options[:file]
262
+ file = options.delete :file
263
+ unless file[0] == ?/
264
+ dirs = [".",
265
+ File.join(File.dirname(__FILE__), "recipes", "templates")]
266
+ dirs.each do |dir|
267
+ if File.file?(File.join(dir, file))
268
+ file = File.join(dir, file)
269
+ break
270
+ elsif File.file?(File.join(dir, file + ".rhtml"))
271
+ file = File.join(dir, file + ".rhtml")
272
+ break
273
+ end
274
+ end
275
+ end
276
+
277
+ render options.merge(:template => File.read(file))
278
+
279
+ when options[:template]
280
+ erb = ERB.new(options[:template])
281
+ b = Proc.new { binding }.call
282
+ options.each do |key, value|
283
+ next if key == :template
284
+ eval "#{key} = options[:#{key}]", b
285
+ end
286
+ erb.result(b)
287
+
288
+ else
289
+ raise ArgumentError, "no file or template given for rendering"
290
+ end
291
+ end
292
+
293
+ # Inspects the remote servers to determine the list of all released versions
294
+ # of the software. Releases are sorted with the most recent release last.
295
+ def releases
296
+ unless @releases
297
+ buffer = ""
298
+ run "ls -x1 #{releases_path}", :once => true do |ch, str, out|
299
+ buffer << out if str == :out
300
+ raise "could not determine releases #{out.inspect}" if str == :err
301
+ end
302
+ @releases = buffer.split.sort
303
+ end
304
+
305
+ @releases
306
+ end
307
+
308
+ # Returns the most recent deployed release
309
+ def current_release
310
+ release_path(releases.last)
311
+ end
312
+
313
+ # Returns the release immediately before the currently deployed one
314
+ def previous_release
315
+ release_path(releases[-2])
316
+ end
317
+
318
+ # Invoke a set of tasks in a transaction. If any task fails (raises an
319
+ # exception), all tasks executed within the transaction are inspected to
320
+ # see if they have an associated on_rollback hook, and if so, that hook
321
+ # is called.
322
+ def transaction
323
+ if task_call_history
324
+ yield
325
+ else
326
+ logger.info "transaction: start"
327
+ begin
328
+ @task_call_history = []
329
+ yield
330
+ logger.info "transaction: commit"
331
+ rescue Object => e
332
+ current = task_call_history.last
333
+ logger.important "transaction: rollback", current ? current.name : "transaction start"
334
+ task_call_history.reverse.each do |task|
335
+ begin
336
+ logger.debug "rolling back", task.name
337
+ task.rollback.call if task.rollback
338
+ rescue Object => e
339
+ logger.info "exception while rolling back: #{e.class}, #{e.message}", task.name
340
+ end
341
+ end
342
+ raise
343
+ ensure
344
+ @task_call_history = nil
345
+ end
346
+ end
347
+ end
348
+
349
+ # Specifies an on_rollback hook for the currently executing task. If this
350
+ # or any subsequent task then fails, and a transaction is active, this
351
+ # hook will be executed.
352
+ def on_rollback(&block)
353
+ task_call_frames.last.rollback = block
354
+ end
355
+
356
+ # An instance-level reader for the class' #default_io_proc attribute.
357
+ def default_io_proc
358
+ self.class.default_io_proc
359
+ end
360
+
361
+ # Used to force connections to be made to the current task's servers.
362
+ # Connections are normally made lazily in Capistrano--you can use this
363
+ # to force them open before performing some operation that might be
364
+ # time-sensitive.
365
+ def connect!(options={})
366
+ execute_on_servers(options) { }
367
+ end
368
+
369
+ def current_task
370
+ return nil if task_call_frames.empty?
371
+ tasks[task_call_frames.last.name]
372
+ end
373
+
374
+ def metaclass
375
+ class << self; self; end
376
+ end
377
+
378
+ private
379
+
380
+ def define_method(name, &block)
381
+ metaclass.send(:define_method, name, &block)
382
+ end
383
+
384
+ def push_task_call_frame(name)
385
+ frame = TaskCallFrame.new(name)
386
+ task_call_frames.push frame
387
+ task_call_history.push frame if task_call_history
388
+ end
389
+
390
+ def pop_task_call_frame
391
+ task_call_frames.pop
392
+ end
393
+
394
+ def establish_connections(servers)
395
+ @factory = establish_gateway if needs_gateway?
396
+ servers.each do |server|
397
+ @sessions[server] ||= @factory.connect_to(server)
398
+ end
399
+ end
400
+
401
+ def establish_gateway
402
+ logger.debug "establishing connection to gateway #{gateway}"
403
+ @established_gateway = true
404
+ Gateway.new(gateway, configuration)
405
+ end
406
+
407
+ def needs_gateway?
408
+ gateway && !@established_gateway
409
+ end
410
+
411
+ def execute_on_servers(options)
412
+ task = current_task
413
+ servers = task.servers
414
+
415
+ if servers.empty?
416
+ raise "The #{task.name} task is only run for servers matching #{task.options.inspect}, but no servers matched"
417
+ end
418
+
419
+ servers = [servers.first] if options[:once]
420
+ logger.trace "servers: #{servers.inspect}"
421
+
422
+ if !pretend
423
+ # establish connections to those servers, as necessary
424
+ establish_connections(servers)
425
+ yield servers
426
+ end
427
+ end
428
+
429
+ def method_missing(sym, *args, &block)
430
+ if @configuration.respond_to?(sym)
431
+ @configuration.send(sym, *args, &block)
432
+ else
433
+ super
434
+ end
435
+ end
436
+
437
+ end
438
+ end