capistrano 2.0.0 → 2.15.2

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 (125) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +7 -0
  4. data/CHANGELOG +715 -18
  5. data/Gemfile +12 -0
  6. data/README.md +94 -0
  7. data/Rakefile +11 -0
  8. data/bin/cap +0 -0
  9. data/bin/capify +37 -22
  10. data/capistrano.gemspec +40 -0
  11. data/lib/capistrano/callback.rb +5 -1
  12. data/lib/capistrano/cli/execute.rb +10 -7
  13. data/lib/capistrano/cli/help.rb +39 -16
  14. data/lib/capistrano/cli/help.txt +44 -16
  15. data/lib/capistrano/cli/options.rb +71 -11
  16. data/lib/capistrano/cli/ui.rb +13 -1
  17. data/lib/capistrano/cli.rb +5 -5
  18. data/lib/capistrano/command.rb +215 -58
  19. data/lib/capistrano/configuration/actions/file_transfer.rb +29 -14
  20. data/lib/capistrano/configuration/actions/inspect.rb +3 -3
  21. data/lib/capistrano/configuration/actions/invocation.rb +212 -22
  22. data/lib/capistrano/configuration/alias_task.rb +26 -0
  23. data/lib/capistrano/configuration/callbacks.rb +26 -27
  24. data/lib/capistrano/configuration/connections.rb +130 -52
  25. data/lib/capistrano/configuration/execution.rb +34 -18
  26. data/lib/capistrano/configuration/loading.rb +91 -6
  27. data/lib/capistrano/configuration/log_formatters.rb +75 -0
  28. data/lib/capistrano/configuration/namespaces.rb +45 -12
  29. data/lib/capistrano/configuration/roles.rb +28 -2
  30. data/lib/capistrano/configuration/servers.rb +51 -10
  31. data/lib/capistrano/configuration/variables.rb +3 -3
  32. data/lib/capistrano/configuration.rb +20 -4
  33. data/lib/capistrano/errors.rb +12 -8
  34. data/lib/capistrano/ext/multistage.rb +62 -0
  35. data/lib/capistrano/ext/string.rb +5 -0
  36. data/lib/capistrano/extensions.rb +1 -1
  37. data/lib/capistrano/fix_rake_deprecated_dsl.rb +8 -0
  38. data/lib/capistrano/logger.rb +112 -5
  39. data/lib/capistrano/processable.rb +55 -0
  40. data/lib/capistrano/recipes/compat.rb +2 -2
  41. data/lib/capistrano/recipes/deploy/assets.rb +185 -0
  42. data/lib/capistrano/recipes/deploy/dependencies.rb +2 -2
  43. data/lib/capistrano/recipes/deploy/local_dependency.rb +10 -2
  44. data/lib/capistrano/recipes/deploy/remote_dependency.rb +54 -2
  45. data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
  46. data/lib/capistrano/recipes/deploy/scm/base.rb +31 -11
  47. data/lib/capistrano/recipes/deploy/scm/bzr.rb +14 -14
  48. data/lib/capistrano/recipes/deploy/scm/cvs.rb +10 -8
  49. data/lib/capistrano/recipes/deploy/scm/darcs.rb +12 -1
  50. data/lib/capistrano/recipes/deploy/scm/git.rb +293 -0
  51. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +23 -15
  52. data/lib/capistrano/recipes/deploy/scm/none.rb +55 -0
  53. data/lib/capistrano/recipes/deploy/scm/perforce.rb +54 -28
  54. data/lib/capistrano/recipes/deploy/scm/subversion.rb +35 -17
  55. data/lib/capistrano/recipes/deploy/scm.rb +1 -1
  56. data/lib/capistrano/recipes/deploy/strategy/base.rb +32 -4
  57. data/lib/capistrano/recipes/deploy/strategy/copy.rb +238 -43
  58. data/lib/capistrano/recipes/deploy/strategy/remote.rb +1 -1
  59. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +11 -1
  60. data/lib/capistrano/recipes/deploy/strategy/unshared_remote_cache.rb +21 -0
  61. data/lib/capistrano/recipes/deploy/strategy.rb +1 -1
  62. data/lib/capistrano/recipes/deploy.rb +265 -123
  63. data/lib/capistrano/recipes/standard.rb +1 -1
  64. data/lib/capistrano/role.rb +102 -0
  65. data/lib/capistrano/server_definition.rb +6 -1
  66. data/lib/capistrano/shell.rb +30 -33
  67. data/lib/capistrano/ssh.rb +46 -60
  68. data/lib/capistrano/task_definition.rb +16 -8
  69. data/lib/capistrano/transfer.rb +218 -0
  70. data/lib/capistrano/version.rb +6 -17
  71. data/lib/capistrano.rb +4 -1
  72. data/test/cli/execute_test.rb +3 -3
  73. data/test/cli/help_test.rb +33 -7
  74. data/test/cli/options_test.rb +109 -6
  75. data/test/cli/ui_test.rb +2 -2
  76. data/test/cli_test.rb +3 -3
  77. data/test/command_test.rb +144 -124
  78. data/test/configuration/actions/file_transfer_test.rb +41 -20
  79. data/test/configuration/actions/inspect_test.rb +21 -7
  80. data/test/configuration/actions/invocation_test.rb +91 -30
  81. data/test/configuration/alias_task_test.rb +118 -0
  82. data/test/configuration/callbacks_test.rb +41 -46
  83. data/test/configuration/connections_test.rb +187 -36
  84. data/test/configuration/execution_test.rb +18 -2
  85. data/test/configuration/loading_test.rb +17 -4
  86. data/test/configuration/namespace_dsl_test.rb +54 -5
  87. data/test/configuration/roles_test.rb +114 -4
  88. data/test/configuration/servers_test.rb +97 -4
  89. data/test/configuration/variables_test.rb +12 -2
  90. data/test/configuration_test.rb +9 -13
  91. data/test/deploy/local_dependency_test.rb +76 -0
  92. data/test/deploy/remote_dependency_test.rb +146 -0
  93. data/test/deploy/scm/accurev_test.rb +23 -0
  94. data/test/deploy/scm/base_test.rb +1 -1
  95. data/test/deploy/scm/bzr_test.rb +51 -0
  96. data/test/deploy/scm/darcs_test.rb +37 -0
  97. data/test/deploy/scm/git_test.rb +221 -0
  98. data/test/deploy/scm/mercurial_test.rb +134 -0
  99. data/test/deploy/scm/none_test.rb +35 -0
  100. data/test/deploy/scm/perforce_test.rb +23 -0
  101. data/test/deploy/scm/subversion_test.rb +40 -0
  102. data/test/deploy/strategy/copy_test.rb +240 -26
  103. data/test/extensions_test.rb +2 -2
  104. data/test/logger_formatting_test.rb +149 -0
  105. data/test/logger_test.rb +13 -2
  106. data/test/recipes_test.rb +25 -0
  107. data/test/role_test.rb +11 -0
  108. data/test/server_definition_test.rb +15 -2
  109. data/test/shell_test.rb +33 -1
  110. data/test/ssh_test.rb +40 -24
  111. data/test/task_definition_test.rb +18 -2
  112. data/test/transfer_test.rb +168 -0
  113. data/test/utils.rb +27 -33
  114. metadata +215 -102
  115. data/MIT-LICENSE +0 -20
  116. data/README +0 -43
  117. data/examples/sample.rb +0 -14
  118. data/lib/capistrano/gateway.rb +0 -131
  119. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +0 -53
  120. data/lib/capistrano/recipes/templates/maintenance.rhtml +0 -53
  121. data/lib/capistrano/recipes/upgrade.rb +0 -33
  122. data/lib/capistrano/upload.rb +0 -146
  123. data/test/gateway_test.rb +0 -167
  124. data/test/upload_test.rb +0 -131
  125. data/test/version_test.rb +0 -24
@@ -23,6 +23,60 @@ module Capistrano
23
23
  def initialize_with_invocation(*args) #:nodoc:
24
24
  initialize_without_invocation(*args)
25
25
  set :default_environment, {}
26
+ set :default_run_options, {}
27
+ end
28
+
29
+ # Executes different commands in parallel. This is useful for commands
30
+ # that need to be different on different hosts, but which could be
31
+ # otherwise run in parallel.
32
+ #
33
+ # The +options+ parameter is currently unused.
34
+ #
35
+ # Example:
36
+ #
37
+ # task :restart_everything do
38
+ # parallel do |session|
39
+ # session.when "in?(:app)", "/path/to/restart/mongrel"
40
+ # session.when "in?(:web)", "/path/to/restart/apache"
41
+ # session.when "in?(:db)", "/path/to/restart/mysql"
42
+ # end
43
+ # end
44
+ #
45
+ # Each command may have its own callback block, for capturing and
46
+ # responding to output, with semantics identical to #run:
47
+ #
48
+ # session.when "in?(:app)", "/path/to/restart/mongrel" do |ch, stream, data|
49
+ # # ch is the SSH channel for this command, used to send data
50
+ # # back to the command (e.g. ch.send_data("password\n"))
51
+ # # stream is either :out or :err, for which stream the data arrived on
52
+ # # data is a string containing data sent from the remote command
53
+ # end
54
+ #
55
+ # Also, you can specify a fallback command, to use when none of the
56
+ # conditions match a server:
57
+ #
58
+ # session.else "/execute/something/else"
59
+ #
60
+ # The string specified as the first argument to +when+ may be any valid
61
+ # Ruby code. It has access to the following variables and methods:
62
+ #
63
+ # * +in?(role)+ returns true if the server participates in the given role
64
+ # * +server+ is the ServerDefinition object for the server. This can be
65
+ # used to get the host-name, etc.
66
+ # * +configuration+ is the current Capistrano::Configuration object, which
67
+ # you can use to get the value of variables, etc.
68
+ #
69
+ # For example:
70
+ #
71
+ # session.when "server.host =~ /app/", "/some/command"
72
+ # session.when "server.host == configuration[:some_var]", "/another/command"
73
+ # session.when "in?(:web) || in?(:app)", "/more/commands"
74
+ #
75
+ # See #run for a description of the valid +options+.
76
+ def parallel(options={})
77
+ raise ArgumentError, "parallel() requires a block" unless block_given?
78
+ tree = Command::Tree.new(self) { |t| yield t }
79
+ run_tree(tree, options)
26
80
  end
27
81
 
28
82
  # Invokes the given command. If a +via+ key is given, it will be used
@@ -41,37 +95,145 @@ module Capistrano
41
95
  # channel (which may be used to send data back to the remote process),
42
96
  # the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
43
97
  # stdout), and the data that was received.
98
+ #
99
+ # The +options+ hash may include any of the following keys:
100
+ #
101
+ # * :hosts - this is either a string (for a single target host) or an array
102
+ # of strings, indicating which hosts the command should run on. By default,
103
+ # the hosts are determined from the task definition.
104
+ # * :roles - this is either a string or symbol (for a single target role) or
105
+ # an array of strings or symbols, indicating which roles the command should
106
+ # run on. If :hosts is specified, :roles will be ignored.
107
+ # * :only - specifies a condition limiting which hosts will be selected to
108
+ # run the command. This should refer to values set in the role definition.
109
+ # For example, if a role is defined with :primary => true, then you could
110
+ # select only hosts with :primary true by setting :only => { :primary => true }.
111
+ # * :except - specifies a condition limiting which hosts will be selected to
112
+ # run the command. This is the inverse of :only (hosts that do _not_ match
113
+ # the condition will be selected).
114
+ # * :on_no_matching_servers - if :continue, will continue to execute tasks if
115
+ # no matching servers are found for the host criteria. The default is to raise
116
+ # a NoMatchingServersError exception.
117
+ # * :once - if true, only the first matching server will be selected. The default
118
+ # is false (all matching servers will be selected).
119
+ # * :max_hosts - specifies the maximum number of hosts that should be selected
120
+ # at a time. If this value is less than the number of hosts that are selected
121
+ # to run, then the hosts will be run in groups of max_hosts. The default is nil,
122
+ # which indicates that there is no maximum host limit. Please note this does not
123
+ # limit the number of SSH channels that can be open, only the number of hosts upon
124
+ # which this will be called.
125
+ # * :shell - says which shell should be used to invoke commands. This
126
+ # defaults to "sh". Setting this to false causes Capistrano to invoke
127
+ # the commands directly, without wrapping them in a shell invocation.
128
+ # * :data - if not nil (the default), this should be a string that will
129
+ # be passed to the command's stdin stream.
130
+ # * :pty - if true, a pseudo-tty will be allocated for each command. The
131
+ # default is false. Note that there are benefits and drawbacks both ways.
132
+ # Empirically, it appears that if a pty is allocated, the SSH server daemon
133
+ # will _not_ read user shell start-up scripts (e.g. bashrc, etc.). However,
134
+ # if a pty is _not_ allocated, some commands will refuse to run in
135
+ # interactive mode and will not prompt for (e.g.) passwords.
136
+ # * :env - a hash of environment variable mappings that should be made
137
+ # available to the command. The keys should be environment variable names,
138
+ # and the values should be their corresponding values. The default is
139
+ # empty, but may be modified by changing the +default_environment+
140
+ # Capistrano variable.
141
+ # * :eof - if true, the standard input stream will be closed after sending
142
+ # any data specified in the :data option. If false, the input stream is
143
+ # left open. The default is to close the input stream only if no block is
144
+ # passed.
145
+ #
146
+ # Note that if you set these keys in the +default_run_options+ Capistrano
147
+ # variable, they will apply for all invocations of #run, #invoke_command,
148
+ # and #parallel.
44
149
  def run(cmd, options={}, &block)
150
+ if options[:eof].nil? && !cmd.include?(sudo)
151
+ options = options.merge(:eof => !block_given?)
152
+ end
45
153
  block ||= self.class.default_io_proc
46
- logger.debug "executing #{cmd.strip.inspect}"
154
+ tree = Command::Tree.new(self) { |t| t.else(cmd, &block) }
155
+ run_tree(tree, options)
156
+ end
47
157
 
158
+ # Executes a Capistrano::Command::Tree object. This is not for direct
159
+ # use, but should instead be called indirectly, via #run or #parallel,
160
+ # or #invoke_command.
161
+ def run_tree(tree, options={}) #:nodoc:
48
162
  options = add_default_command_options(options)
49
163
 
164
+ if tree.branches.any? || tree.fallback
165
+ _, servers = filter_servers(options)
166
+ branches = servers.map{|server| tree.branches_for(server)}.compact
167
+ case branches.size
168
+ when 0
169
+ branches = tree.branches.dup + [tree.fallback]
170
+ case branches.size
171
+ when 1
172
+ logger.debug "no servers for #{branches.first}"
173
+ else
174
+ logger.debug "no servers for commands"
175
+ branches.each{ |branch| logger.trace "-> #{branch.to_s(true)}" }
176
+ end
177
+ when 1
178
+ logger.debug "executing #{branches.first}" unless options[:silent]
179
+ else
180
+ logger.debug "executing multiple commands in parallel"
181
+ branches.each{ |branch| logger.trace "-> #{branch.to_s(true)}" }
182
+ end
183
+ else
184
+ raise ArgumentError, "attempt to execute without specifying a command"
185
+ end
186
+
187
+ return if dry_run || (debug && continue_execution(tree) == false)
188
+
189
+ tree.each do |branch|
190
+ if branch.command.include?(sudo)
191
+ branch.callback = sudo_behavior_callback(branch.callback)
192
+ end
193
+ end
194
+
50
195
  execute_on_servers(options) do |servers|
51
196
  targets = servers.map { |s| sessions[s] }
52
- Command.process(cmd, targets, options.merge(:logger => logger), &block)
197
+ Command.process(tree, targets, options.merge(:logger => logger))
53
198
  end
54
199
  end
55
200
 
56
- # Like #run, but executes the command via <tt>sudo</tt>. This assumes
57
- # that the sudo password (if required) is the same as the password for
58
- # logging in to the server.
201
+ # Returns the command string used by capistrano to invoke a comamnd via
202
+ # sudo.
203
+ #
204
+ # run "#{sudo :as => 'bob'} mkdir /path/to/dir"
205
+ #
206
+ # It can also be invoked like #run, but executing the command via sudo.
207
+ # This assumes that the sudo password (if required) is the same as the
208
+ # password for logging in to the server.
59
209
  #
60
- # Also, this module accepts a <tt>:sudo</tt> configuration variable,
210
+ # sudo "mkdir /path/to/dir"
211
+ #
212
+ # Also, this method understands a <tt>:sudo</tt> configuration variable,
61
213
  # which (if specified) will be used as the full path to the sudo
62
214
  # executable on the remote machine:
63
215
  #
64
216
  # set :sudo, "/opt/local/bin/sudo"
65
- def sudo(command, options={}, &block)
66
- block ||= self.class.default_io_proc
67
-
68
- options = options.dup
69
- as = options.delete(:as)
217
+ #
218
+ # If you know what you're doing, you can also set <tt>:sudo_prompt</tt>,
219
+ # which tells capistrano which prompt sudo should use when asking for
220
+ # a password. (This is so that capistrano knows what prompt to look for
221
+ # in the output.) If you set :sudo_prompt to an empty string, Capistrano
222
+ # will not send a preferred prompt.
223
+ def sudo(*parameters, &block)
224
+ options = parameters.last.is_a?(Hash) ? parameters.pop.dup : {}
225
+ command = parameters.first
226
+ user = options[:as] && "-u #{options.delete(:as)}"
70
227
 
71
- user = as && "-u #{as}"
72
- command = [fetch(:sudo, "sudo"), user, command].compact.join(" ")
228
+ sudo_prompt_option = "-p '#{sudo_prompt}'" unless sudo_prompt.empty?
229
+ sudo_command = [fetch(:sudo, "sudo"), sudo_prompt_option, user].compact.join(" ")
73
230
 
74
- run(command, options, &sudo_behavior_callback(block))
231
+ if command
232
+ command = sudo_command + " " + command
233
+ run(command, options, &block)
234
+ else
235
+ return sudo_command
236
+ end
75
237
  end
76
238
 
77
239
  # Returns a Proc object that defines the behavior of the sudo
@@ -83,17 +245,19 @@ module Capistrano
83
245
  # was wrong, let's track which host prompted first and only allow
84
246
  # subsequent prompts from that host.
85
247
  prompt_host = nil
86
-
248
+
87
249
  Proc.new do |ch, stream, out|
88
- if out =~ /password:/i
89
- ch.send_data "#{self[:password]}\n"
90
- elsif out =~ /try again/
250
+ if out =~ /^Sorry, try again/
91
251
  if prompt_host.nil? || prompt_host == ch[:server]
92
252
  prompt_host = ch[:server]
93
253
  logger.important out, "#{stream} :: #{ch[:server]}"
94
254
  reset! :password
95
255
  end
96
- else
256
+ end
257
+
258
+ if out =~ /^#{Regexp.escape(sudo_prompt)}/
259
+ ch.send_data "#{self[:password]}\n"
260
+ elsif fallback
97
261
  fallback.call(ch, stream, out)
98
262
  end
99
263
  end
@@ -110,18 +274,44 @@ module Capistrano
110
274
  # Otherwise, if the :default_shell key exists in the configuration,
111
275
  # it will be used. Otherwise, no :shell key is added.
112
276
  def add_default_command_options(options)
113
- options = options.dup
277
+ defaults = self[:default_run_options]
278
+ options = defaults.merge(options)
114
279
 
115
280
  env = self[:default_environment]
116
281
  env = env.merge(options[:env]) if options[:env]
117
282
  options[:env] = env unless env.empty?
118
283
 
119
284
  shell = options[:shell] || self[:default_shell]
120
- options[:shell] = shell if shell
285
+ options[:shell] = shell unless shell.nil?
121
286
 
122
287
  options
123
288
  end
289
+
290
+ # Returns the prompt text to use with sudo
291
+ def sudo_prompt
292
+ fetch(:sudo_prompt, "sudo password: ")
293
+ end
294
+
295
+ def continue_execution(tree)
296
+ if tree.branches.length == 1
297
+ continue_execution_for_branch(tree.branches.first)
298
+ else
299
+ tree.each { |branch| branch.skip! unless continue_execution_for_branch(branch) }
300
+ tree.any? { |branch| !branch.skip? }
301
+ end
302
+ end
303
+
304
+ def continue_execution_for_branch(branch)
305
+ case Capistrano::CLI.debug_prompt(branch)
306
+ when "y"
307
+ true
308
+ when "n"
309
+ false
310
+ when "a"
311
+ exit(-1)
312
+ end
313
+ end
124
314
  end
125
315
  end
126
316
  end
127
- end
317
+ end
@@ -0,0 +1,26 @@
1
+ module Capistrano
2
+ class Configuration
3
+ module AliasTask
4
+ # Attempts to find the task at the given fully-qualified path, and
5
+ # alias it. If arguments don't have correct task names, an ArgumentError
6
+ # wil be raised. If no such task exists, a Capistrano::NoSuchTaskError
7
+ # will be raised.
8
+ #
9
+ # Usage:
10
+ #
11
+ # alias_task :original_deploy, :deploy
12
+ #
13
+ def alias_task(new_name, old_name)
14
+ if !new_name.respond_to?(:to_sym) or !old_name.respond_to?(:to_sym)
15
+ raise ArgumentError, "expected a valid task name"
16
+ end
17
+
18
+ original_task = find_task(old_name) or raise NoSuchTaskError, "the task `#{old_name}' does not exist"
19
+ task = original_task.dup # Dup. task to avoid modify original task
20
+ task.name = new_name
21
+
22
+ define_task(task)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -4,7 +4,7 @@ module Capistrano
4
4
  class Configuration
5
5
  module Callbacks
6
6
  def self.included(base) #:nodoc:
7
- %w(initialize execute_task).each do |method|
7
+ %w(initialize invoke_task_directly).each do |method|
8
8
  base.send :alias_method, "#{method}_without_callbacks", method
9
9
  base.send :alias_method, method, "#{method}_with_callbacks"
10
10
  end
@@ -18,19 +18,14 @@ module Capistrano
18
18
  @callbacks = {}
19
19
  end
20
20
 
21
- def execute_task_with_callbacks(task) #:nodoc:
22
- before = find_hook(task, :before)
23
- execute_task(before) if before
21
+ def invoke_task_directly_with_callbacks(task) #:nodoc:
24
22
 
25
23
  trigger :before, task
26
24
 
27
- result = execute_task_without_callbacks(task)
25
+ result = invoke_task_directly_without_callbacks(task)
28
26
 
29
27
  trigger :after, task
30
28
 
31
- after = find_hook(task, :after)
32
- execute_task(after) if after
33
-
34
29
  return result
35
30
  end
36
31
 
@@ -95,7 +90,7 @@ module Capistrano
95
90
  # Usage:
96
91
  #
97
92
  # on :before, "some:hook", "another:hook", :only => "deploy:update"
98
- # on :after, "some:hook", :except => "deploy:symlink"
93
+ # on :after, "some:hook", :except => "deploy:create_symlink"
99
94
  # on :before, "global:hook"
100
95
  # on :after, :only => :deploy do
101
96
  # puts "after deploy here"
@@ -111,10 +106,28 @@ module Capistrano
111
106
  elsif block
112
107
  callbacks[event] << ProcCallback.new(block, options)
113
108
  else
114
- args.each do |name|
115
- callbacks[event] << TaskCallback.new(self, name, options)
116
- end
109
+ args = filter_deprecated_tasks(args)
110
+ options[:only] = filter_deprecated_tasks(options[:only])
111
+ options[:except] = filter_deprecated_tasks(options[:except])
112
+
113
+ callbacks[event].concat(args.map { |name| TaskCallback.new(self, name, options) })
114
+ end
115
+ end
116
+
117
+ # Filters the given task name or names and attempts to replace deprecated tasks with their equivalents.
118
+ def filter_deprecated_tasks(names)
119
+ deprecation_msg = "[Deprecation Warning] This API has changed, please hook `deploy:create_symlink` instead of" \
120
+ " `deploy:symlink`."
121
+
122
+ if names == "deploy:symlink"
123
+ warn deprecation_msg
124
+ names = "deploy:create_symlink"
125
+ elsif names.is_a?(Array) && names.include?("deploy:symlink")
126
+ warn deprecation_msg
127
+ names = names.map { |name| name == "deploy:symlink" ? "deploy:create_symlink" : name }
117
128
  end
129
+
130
+ names
118
131
  end
119
132
 
120
133
  # Trigger the named event for the named task. All associated callbacks
@@ -129,20 +142,6 @@ module Capistrano
129
142
  end
130
143
  end
131
144
 
132
- private
133
-
134
- # Looks for before_foo or after_foo tasks. This method of extending tasks
135
- # is now discouraged (though not formally deprecated). You should use the
136
- # before and after methods to declare hooks for such callbacks.
137
- def find_hook(task, hook)
138
- if task == task.namespace.default_task
139
- result = task.namespace.search_task("#{hook}_#{task.namespace.name}")
140
- return result if result
141
- end
142
-
143
- task.namespace.search_task("#{hook}_#{task.name}")
144
- end
145
-
146
145
  end
147
146
  end
148
- end
147
+ end
@@ -1,5 +1,7 @@
1
- require 'capistrano/gateway'
1
+ require 'enumerator'
2
+ require 'net/ssh/gateway'
2
3
  require 'capistrano/ssh'
4
+ require 'capistrano/errors'
3
5
 
4
6
  module Capistrano
5
7
  class Configuration
@@ -9,8 +11,6 @@ module Capistrano
9
11
  base.send :alias_method, :initialize, :initialize_with_connections
10
12
  end
11
13
 
12
- # An adaptor for making the SSH interface look and act like that of the
13
- # Gateway class.
14
14
  class DefaultConnectionFactory #:nodoc:
15
15
  def initialize(options)
16
16
  @options = options
@@ -21,27 +21,76 @@ module Capistrano
21
21
  end
22
22
  end
23
23
 
24
+ class GatewayConnectionFactory #:nodoc:
25
+ def initialize(gateway, options)
26
+ @options = options
27
+ Thread.abort_on_exception = true
28
+ @gateways = {}
29
+ if gateway.is_a?(Hash)
30
+ @options[:logger].debug "Creating multiple gateways using #{gateway.inspect}" if @options[:logger]
31
+ gateway.each do |gw, hosts|
32
+ gateway_connection = add_gateway(gw)
33
+ [*hosts].each do |host|
34
+ @gateways[:default] ||= gateway_connection
35
+ @gateways[host] = gateway_connection
36
+ end
37
+ end
38
+ else
39
+ @options[:logger].debug "Creating gateway using #{[*gateway].join(', ')}" if @options[:logger]
40
+ @gateways[:default] = add_gateway(gateway)
41
+ end
42
+ end
43
+
44
+ def add_gateway(gateway)
45
+ gateways = [*gateway].collect { |g| ServerDefinition.new(g) }
46
+ tunnel = SSH.connection_strategy(gateways[0], @options) do |host, user, connect_options|
47
+ Net::SSH::Gateway.new(host, user, connect_options)
48
+ end
49
+ (gateways[1..-1]).inject(tunnel) do |tunnel, destination|
50
+ @options[:logger].debug "Creating tunnel to #{destination}" if @options[:logger]
51
+ local_host = ServerDefinition.new("127.0.0.1", :user => destination.user, :port => tunnel.open(destination.host, (destination.port || 22)))
52
+ SSH.connection_strategy(local_host, @options) do |host, user, connect_options|
53
+ Net::SSH::Gateway.new(host, user, connect_options)
54
+ end
55
+ end
56
+ end
57
+
58
+ def connect_to(server)
59
+ @options[:logger].debug "establishing connection to `#{server}' via gateway" if @options[:logger]
60
+ local_host = ServerDefinition.new("127.0.0.1", :user => server.user, :port => gateway_for(server).open(server.host, server.port || 22))
61
+ session = SSH.connect(local_host, @options)
62
+ session.xserver = server
63
+ session
64
+ end
65
+
66
+ def gateway_for(server)
67
+ @gateways[server.host] || @gateways[:default]
68
+ end
69
+ end
70
+
24
71
  # A hash of the SSH sessions that are currently open and available.
25
72
  # Because sessions are constructed lazily, this will only contain
26
73
  # connections to those servers that have been the targets of one or more
27
- # executed tasks.
28
- attr_reader :sessions
74
+ # executed tasks. Stored on a per-thread basis to improve thread-safety.
75
+ def sessions
76
+ Thread.current[:sessions] ||= {}
77
+ end
29
78
 
30
79
  def initialize_with_connections(*args) #:nodoc:
31
80
  initialize_without_connections(*args)
32
- @sessions = {}
33
- @failed_sessions = []
81
+ Thread.current[:sessions] = {}
82
+ Thread.current[:failed_sessions] = []
34
83
  end
35
84
 
36
85
  # Indicate that the given server could not be connected to.
37
86
  def failed!(server)
38
- @failed_sessions << server
87
+ Thread.current[:failed_sessions] << server
39
88
  end
40
89
 
41
90
  # Query whether previous connection attempts to the given server have
42
91
  # failed.
43
92
  def has_failed?(server)
44
- @failed_sessions.include?(server)
93
+ Thread.current[:failed_sessions].include?(server)
45
94
  end
46
95
 
47
96
  # Used to force connections to be made to the current task's servers.
@@ -57,9 +106,9 @@ module Capistrano
57
106
  # establish connections to servers defined via ServerDefinition objects.
58
107
  def connection_factory
59
108
  @connection_factory ||= begin
60
- if exists?(:gateway)
61
- logger.debug "establishing connection to gateway `#{fetch(:gateway)}'"
62
- Gateway.new(ServerDefinition.new(fetch(:gateway)), self)
109
+ if exists?(:gateway) && !fetch(:gateway).nil? && !fetch(:gateway).empty?
110
+ logger.debug "establishing connection to gateway `#{fetch(:gateway).inspect}'"
111
+ GatewayConnectionFactory.new(fetch(:gateway), self)
63
112
  else
64
113
  DefaultConnectionFactory.new(self)
65
114
  end
@@ -70,22 +119,13 @@ module Capistrano
70
119
  def establish_connections_to(servers)
71
120
  failed_servers = []
72
121
 
73
- # This attemps to work around the problem where SFTP uploads hang
74
- # for some people. A bit of investigating seemed to reveal that the
75
- # hang only occurred when the SSH connections were established async,
76
- # so this setting allows people to at least work around the problem.
77
- if fetch(:synchronous_connect, false)
78
- logger.trace "synchronous_connect: true"
79
- Array(servers).each { |server| safely_establish_connection_to(server, failed_servers) }
80
- else
81
- # force the connection factory to be instantiated synchronously,
82
- # otherwise we wind up with multiple gateway instances, because
83
- # each connection is done in parallel.
84
- connection_factory
122
+ # force the connection factory to be instantiated synchronously,
123
+ # otherwise we wind up with multiple gateway instances, because
124
+ # each connection is done in parallel.
125
+ connection_factory
85
126
 
86
- threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
87
- threads.each { |t| t.join }
88
- end
127
+ threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
128
+ threads.each { |t| t.join }
89
129
 
90
130
  if failed_servers.any?
91
131
  errors = failed_servers.map { |h| "#{h[:server]} (#{h[:error].class}: #{h[:error].message})" }
@@ -95,47 +135,83 @@ module Capistrano
95
135
  end
96
136
  end
97
137
 
98
- # Determines the set of servers within the current task's scope and
99
- # establishes connections to them, and then yields that list of
100
- # servers.
101
- def execute_on_servers(options={})
102
- raise ArgumentError, "expected a block" unless block_given?
138
+ # Destroys sessions for each server in the list.
139
+ def teardown_connections_to(servers)
140
+ servers.each do |server|
141
+ begin
142
+ session = sessions.delete(server)
143
+ session.close if session
144
+ rescue IOError, Net::SSH::Disconnect
145
+ # the TCP connection is already dead
146
+ end
147
+ end
148
+ end
103
149
 
150
+ # Determines the set of servers within the current task's scope
151
+ def filter_servers(options={})
104
152
  if task = current_task
105
153
  servers = find_servers_for_task(task, options)
106
154
 
107
155
  if servers.empty?
108
- raise Capistrano::NoMatchingServersError, "`#{task.fully_qualified_name}' is only run for servers matching #{task.options.inspect}, but no servers matched"
156
+ if ENV['HOSTFILTER'] || task.options.merge(options)[:on_no_matching_servers] == :continue
157
+ logger.info "skipping `#{task.fully_qualified_name}' because no servers matched"
158
+ else
159
+ unless dry_run
160
+ raise Capistrano::NoMatchingServersError, "`#{task.fully_qualified_name}' is only run for servers matching #{task.options.inspect}, but no servers matched"
161
+ end
162
+ end
109
163
  end
110
164
 
111
165
  if task.continue_on_error?
112
166
  servers.delete_if { |s| has_failed?(s) }
113
- return if servers.empty?
114
167
  end
115
168
  else
116
169
  servers = find_servers(options)
117
- raise Capistrano::NoMatchingServersError, "no servers found to match #{options.inspect}" if servers.empty?
170
+ if servers.empty? && !dry_run
171
+ raise Capistrano::NoMatchingServersError, "no servers found to match #{options.inspect}" if options[:on_no_matching_servers] != :continue
172
+ end
118
173
  end
119
174
 
120
175
  servers = [servers.first] if options[:once]
176
+ [task, servers.compact]
177
+ end
178
+
179
+ # Determines the set of servers within the current task's scope and
180
+ # establishes connections to them, and then yields that list of
181
+ # servers.
182
+ def execute_on_servers(options={})
183
+ raise ArgumentError, "expected a block" unless block_given?
184
+
185
+ task, servers = filter_servers(options)
186
+ return if servers.empty?
121
187
  logger.trace "servers: #{servers.map { |s| s.host }.inspect}"
122
188
 
123
- # establish connections to those servers, as necessary
124
- begin
125
- establish_connections_to(servers)
126
- rescue ConnectionError => error
127
- raise error unless task && task.continue_on_error?
128
- error.hosts.each do |h|
129
- servers.delete(h)
130
- failed!(h)
189
+ max_hosts = (options[:max_hosts] || (task && task.max_hosts) || servers.size).to_i
190
+ is_subset = max_hosts < servers.size
191
+
192
+ # establish connections to those servers in groups of max_hosts, as necessary
193
+ servers.each_slice(max_hosts) do |servers_slice|
194
+ begin
195
+ establish_connections_to(servers_slice)
196
+ rescue ConnectionError => error
197
+ raise error unless task && task.continue_on_error?
198
+ error.hosts.each do |h|
199
+ servers_slice.delete(h)
200
+ failed!(h)
201
+ end
202
+ end
203
+
204
+ begin
205
+ yield servers_slice
206
+ rescue RemoteError => error
207
+ raise error unless task && task.continue_on_error?
208
+ error.hosts.each { |h| failed!(h) }
131
209
  end
132
- end
133
210
 
134
- begin
135
- yield servers
136
- rescue RemoteError => error
137
- raise error unless task && task.continue_on_error?
138
- error.hosts.each { |h| failed!(h) }
211
+ # if dealing with a subset (e.g., :max_hosts is less than the
212
+ # number of servers available) teardown the subset of connections
213
+ # that were just made, so that we can make room for the next subset.
214
+ teardown_connections_to(servers_slice) if is_subset
139
215
  end
140
216
  end
141
217
 
@@ -145,11 +221,13 @@ module Capistrano
145
221
  # prevents problems with the thread's scope seeing the wrong 'server'
146
222
  # variable if the thread just happens to take too long to start up.
147
223
  def establish_connection_to(server, failures=nil)
148
- Thread.new { safely_establish_connection_to(server, failures) }
224
+ current_thread = Thread.current
225
+ Thread.new { safely_establish_connection_to(server, current_thread, failures) }
149
226
  end
150
227
 
151
- def safely_establish_connection_to(server, failures=nil)
152
- sessions[server] ||= connection_factory.connect_to(server)
228
+ def safely_establish_connection_to(server, thread, failures=nil)
229
+ thread[:sessions] ||= {}
230
+ thread[:sessions][server] ||= connection_factory.connect_to(server)
153
231
  rescue Exception => err
154
232
  raise unless failures
155
233
  failures << { :server => server, :error => err }