capistrano 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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