wulffeld-capistrano 2.5.8

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 (105) hide show
  1. data/CHANGELOG.rdoc +761 -0
  2. data/Manifest +104 -0
  3. data/README.rdoc +66 -0
  4. data/Rakefile +34 -0
  5. data/bin/cap +4 -0
  6. data/bin/capify +78 -0
  7. data/examples/sample.rb +14 -0
  8. data/lib/capistrano.rb +2 -0
  9. data/lib/capistrano/callback.rb +45 -0
  10. data/lib/capistrano/cli.rb +47 -0
  11. data/lib/capistrano/cli/execute.rb +84 -0
  12. data/lib/capistrano/cli/help.rb +125 -0
  13. data/lib/capistrano/cli/help.txt +75 -0
  14. data/lib/capistrano/cli/options.rb +224 -0
  15. data/lib/capistrano/cli/ui.rb +40 -0
  16. data/lib/capistrano/command.rb +283 -0
  17. data/lib/capistrano/configuration.rb +43 -0
  18. data/lib/capistrano/configuration/actions/file_transfer.rb +47 -0
  19. data/lib/capistrano/configuration/actions/inspect.rb +46 -0
  20. data/lib/capistrano/configuration/actions/invocation.rb +293 -0
  21. data/lib/capistrano/configuration/callbacks.rb +148 -0
  22. data/lib/capistrano/configuration/connections.rb +200 -0
  23. data/lib/capistrano/configuration/execution.rb +132 -0
  24. data/lib/capistrano/configuration/loading.rb +197 -0
  25. data/lib/capistrano/configuration/namespaces.rb +197 -0
  26. data/lib/capistrano/configuration/roles.rb +73 -0
  27. data/lib/capistrano/configuration/servers.rb +85 -0
  28. data/lib/capistrano/configuration/variables.rb +127 -0
  29. data/lib/capistrano/errors.rb +15 -0
  30. data/lib/capistrano/extensions.rb +57 -0
  31. data/lib/capistrano/logger.rb +59 -0
  32. data/lib/capistrano/processable.rb +53 -0
  33. data/lib/capistrano/recipes/compat.rb +32 -0
  34. data/lib/capistrano/recipes/deploy.rb +562 -0
  35. data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
  36. data/lib/capistrano/recipes/deploy/local_dependency.rb +54 -0
  37. data/lib/capistrano/recipes/deploy/remote_dependency.rb +105 -0
  38. data/lib/capistrano/recipes/deploy/scm.rb +19 -0
  39. data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
  40. data/lib/capistrano/recipes/deploy/scm/base.rb +196 -0
  41. data/lib/capistrano/recipes/deploy/scm/bzr.rb +83 -0
  42. data/lib/capistrano/recipes/deploy/scm/cvs.rb +152 -0
  43. data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
  44. data/lib/capistrano/recipes/deploy/scm/git.rb +302 -0
  45. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
  46. data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
  47. data/lib/capistrano/recipes/deploy/scm/perforce.rb +133 -0
  48. data/lib/capistrano/recipes/deploy/scm/subversion.rb +121 -0
  49. data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
  50. data/lib/capistrano/recipes/deploy/strategy/base.rb +79 -0
  51. data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
  52. data/lib/capistrano/recipes/deploy/strategy/copy.rb +210 -0
  53. data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
  54. data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
  55. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +56 -0
  56. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
  57. data/lib/capistrano/recipes/standard.rb +37 -0
  58. data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
  59. data/lib/capistrano/recipes/upgrade.rb +33 -0
  60. data/lib/capistrano/role.rb +102 -0
  61. data/lib/capistrano/server_definition.rb +56 -0
  62. data/lib/capistrano/shell.rb +260 -0
  63. data/lib/capistrano/ssh.rb +99 -0
  64. data/lib/capistrano/task_definition.rb +70 -0
  65. data/lib/capistrano/transfer.rb +216 -0
  66. data/lib/capistrano/version.rb +18 -0
  67. data/setup.rb +1346 -0
  68. data/test/cli/execute_test.rb +132 -0
  69. data/test/cli/help_test.rb +165 -0
  70. data/test/cli/options_test.rb +317 -0
  71. data/test/cli/ui_test.rb +28 -0
  72. data/test/cli_test.rb +17 -0
  73. data/test/command_test.rb +286 -0
  74. data/test/configuration/actions/file_transfer_test.rb +61 -0
  75. data/test/configuration/actions/inspect_test.rb +65 -0
  76. data/test/configuration/actions/invocation_test.rb +224 -0
  77. data/test/configuration/callbacks_test.rb +220 -0
  78. data/test/configuration/connections_test.rb +349 -0
  79. data/test/configuration/execution_test.rb +175 -0
  80. data/test/configuration/loading_test.rb +132 -0
  81. data/test/configuration/namespace_dsl_test.rb +311 -0
  82. data/test/configuration/roles_test.rb +144 -0
  83. data/test/configuration/servers_test.rb +121 -0
  84. data/test/configuration/variables_test.rb +184 -0
  85. data/test/configuration_test.rb +88 -0
  86. data/test/deploy/local_dependency_test.rb +76 -0
  87. data/test/deploy/remote_dependency_test.rb +114 -0
  88. data/test/deploy/scm/accurev_test.rb +23 -0
  89. data/test/deploy/scm/base_test.rb +55 -0
  90. data/test/deploy/scm/git_test.rb +167 -0
  91. data/test/deploy/scm/mercurial_test.rb +129 -0
  92. data/test/deploy/strategy/copy_test.rb +258 -0
  93. data/test/extensions_test.rb +69 -0
  94. data/test/fixtures/cli_integration.rb +5 -0
  95. data/test/fixtures/config.rb +5 -0
  96. data/test/fixtures/custom.rb +3 -0
  97. data/test/logger_test.rb +123 -0
  98. data/test/role_test.rb +11 -0
  99. data/test/server_definition_test.rb +121 -0
  100. data/test/shell_test.rb +90 -0
  101. data/test/ssh_test.rb +104 -0
  102. data/test/task_definition_test.rb +101 -0
  103. data/test/transfer_test.rb +160 -0
  104. data/test/utils.rb +38 -0
  105. metadata +207 -0
@@ -0,0 +1,283 @@
1
+ require 'capistrano/errors'
2
+ require 'capistrano/processable'
3
+
4
+ module Capistrano
5
+
6
+ # This class encapsulates a single command to be executed on a set of remote
7
+ # machines, in parallel.
8
+ class Command
9
+ include Processable
10
+
11
+ class Tree
12
+ attr_reader :configuration
13
+ attr_reader :branches
14
+ attr_reader :fallback
15
+
16
+ include Enumerable
17
+
18
+ class Branch
19
+ attr_accessor :command, :callback
20
+ attr_reader :options
21
+
22
+ def initialize(command, options, callback)
23
+ @command = command.strip.gsub(/\r?\n/, "\\\n")
24
+ @callback = callback || Capistrano::Configuration.default_io_proc
25
+ @options = options
26
+ @skip = false
27
+ end
28
+
29
+ def last?
30
+ options[:last]
31
+ end
32
+
33
+ def skip?
34
+ @skip
35
+ end
36
+
37
+ def skip!
38
+ @skip = true
39
+ end
40
+
41
+ def match(server)
42
+ true
43
+ end
44
+
45
+ def to_s
46
+ command.inspect
47
+ end
48
+ end
49
+
50
+ class ConditionBranch < Branch
51
+ attr_accessor :configuration
52
+ attr_accessor :condition
53
+
54
+ class Evaluator
55
+ attr_reader :configuration, :condition, :server
56
+
57
+ def initialize(config, condition, server)
58
+ @configuration = config
59
+ @condition = condition
60
+ @server = server
61
+ end
62
+
63
+ def in?(role)
64
+ configuration.roles[role].include?(server)
65
+ end
66
+
67
+ def result
68
+ eval(condition, binding)
69
+ end
70
+
71
+ def method_missing(sym, *args, &block)
72
+ if server.respond_to?(sym)
73
+ server.send(sym, *args, &block)
74
+ elsif configuration.respond_to?(sym)
75
+ configuration.send(sym, *args, &block)
76
+ else
77
+ super
78
+ end
79
+ end
80
+ end
81
+
82
+ def initialize(configuration, condition, command, options, callback)
83
+ @configuration = configuration
84
+ @condition = condition
85
+ super(command, options, callback)
86
+ end
87
+
88
+ def match(server)
89
+ Evaluator.new(configuration, condition, server).result
90
+ end
91
+
92
+ def to_s
93
+ "#{condition.inspect} :: #{command.inspect}"
94
+ end
95
+ end
96
+
97
+ def initialize(config)
98
+ @configuration = config
99
+ @branches = []
100
+ yield self if block_given?
101
+ end
102
+
103
+ def when(condition, command, options={}, &block)
104
+ branches << ConditionBranch.new(configuration, condition, command, options, block)
105
+ end
106
+
107
+ def else(command, &block)
108
+ @fallback = Branch.new(command, {}, block)
109
+ end
110
+
111
+ def branches_for(server)
112
+ seen_last = false
113
+ matches = branches.select do |branch|
114
+ success = !seen_last && !branch.skip? && branch.match(server)
115
+ seen_last = success && branch.last?
116
+ success
117
+ end
118
+
119
+ matches << fallback if matches.empty? && fallback
120
+ return matches
121
+ end
122
+
123
+ def each
124
+ branches.each { |branch| yield branch }
125
+ yield fallback if fallback
126
+ return self
127
+ end
128
+ end
129
+
130
+ attr_reader :tree, :sessions, :options
131
+
132
+ def self.process(tree, sessions, options={})
133
+ new(tree, sessions, options).process!
134
+ end
135
+
136
+ # Instantiates a new command object. The +command+ must be a string
137
+ # containing the command to execute. +sessions+ is an array of Net::SSH
138
+ # session instances, and +options+ must be a hash containing any of the
139
+ # following keys:
140
+ #
141
+ # * +logger+: (optional), a Capistrano::Logger instance
142
+ # * +data+: (optional), a string to be sent to the command via it's stdin
143
+ # * +env+: (optional), a string or hash to be interpreted as environment
144
+ # variables that should be defined for this command invocation.
145
+ def initialize(tree, sessions, options={}, &block)
146
+ if String === tree
147
+ tree = Tree.new(nil) { |t| t.else(tree, &block) }
148
+ elsif block
149
+ raise ArgumentError, "block given with tree argument"
150
+ end
151
+
152
+ @tree = tree
153
+ @sessions = sessions
154
+ @options = options
155
+ @channels = open_channels
156
+ end
157
+
158
+ # Processes the command in parallel on all specified hosts. If the command
159
+ # fails (non-zero return code) on any of the hosts, this will raise a
160
+ # Capistrano::CommandError.
161
+ def process!
162
+ loop do
163
+ break unless process_iteration { @channels.any? { |ch| !ch[:closed] } }
164
+ end
165
+
166
+ logger.trace "command finished" if logger
167
+
168
+ if (failed = @channels.select { |ch| ch[:status] != 0 }).any?
169
+ commands = failed.inject({}) { |map, ch| (map[ch[:command]] ||= []) << ch[:server]; map }
170
+ message = commands.map { |command, list| "#{command.inspect} on #{list.join(',')}" }.join("; ")
171
+ error = CommandError.new("failed: #{message}")
172
+ error.hosts = commands.values.flatten
173
+ raise error
174
+ end
175
+
176
+ self
177
+ end
178
+
179
+ # Force the command to stop processing, by closing all open channels
180
+ # associated with this command.
181
+ def stop!
182
+ @channels.each do |ch|
183
+ ch.close unless ch[:closed]
184
+ end
185
+ end
186
+
187
+ private
188
+
189
+ def logger
190
+ options[:logger]
191
+ end
192
+
193
+ def open_channels
194
+ sessions.map do |session|
195
+ server = session.xserver
196
+ tree.branches_for(server).map do |branch|
197
+ session.open_channel do |channel|
198
+ channel[:server] = server
199
+ channel[:host] = server.host
200
+ channel[:options] = options
201
+ channel[:branch] = branch
202
+
203
+ request_pty_if_necessary(channel) do |ch, success|
204
+ if success
205
+ logger.trace "executing command", ch[:server] if logger
206
+ cmd = replace_placeholders(channel[:branch].command, ch)
207
+
208
+ if options[:shell] == false
209
+ shell = nil
210
+ else
211
+ shell = "#{options[:shell] || "sh"} -c"
212
+ cmd = cmd.gsub(/[$\\`"]/) { |m| "\\#{m}" }
213
+ cmd = "\"#{cmd}\""
214
+ end
215
+
216
+ command_line = [environment, shell, cmd].compact.join(" ")
217
+ ch[:command] = command_line
218
+
219
+ ch.exec(command_line)
220
+ ch.send_data(options[:data]) if options[:data]
221
+ else
222
+ # just log it, don't actually raise an exception, since the
223
+ # process method will see that the status is not zero and will
224
+ # raise an exception then.
225
+ logger.important "could not open channel", ch[:server] if logger
226
+ ch.close
227
+ end
228
+ end
229
+
230
+ channel.on_data do |ch, data|
231
+ ch[:branch].callback[ch, :out, data]
232
+ end
233
+
234
+ channel.on_extended_data do |ch, type, data|
235
+ ch[:branch].callback[ch, :err, data]
236
+ end
237
+
238
+ channel.on_request("exit-status") do |ch, data|
239
+ ch[:status] = data.read_long
240
+ end
241
+
242
+ channel.on_close do |ch|
243
+ ch[:closed] = true
244
+ end
245
+ end
246
+ end
247
+ end.flatten
248
+ end
249
+
250
+ def request_pty_if_necessary(channel)
251
+ if options[:pty]
252
+ channel.request_pty do |ch, success|
253
+ yield ch, success
254
+ end
255
+ else
256
+ yield channel, true
257
+ end
258
+ end
259
+
260
+ def replace_placeholders(command, channel)
261
+ command.gsub(/\$CAPISTRANO:HOST\$/, channel[:host])
262
+ end
263
+
264
+ # prepare a space-separated sequence of variables assignments
265
+ # intended to be prepended to a command, so the shell sets
266
+ # the environment before running the command.
267
+ # i.e.: options[:env] = {'PATH' => '/opt/ruby/bin:$PATH',
268
+ # 'TEST' => '( "quoted" )'}
269
+ # environment returns:
270
+ # "env TEST=(\ \"quoted\"\ ) PATH=/opt/ruby/bin:$PATH"
271
+ def environment
272
+ return if options[:env].nil? || options[:env].empty?
273
+ @environment ||= if String === options[:env]
274
+ "env #{options[:env]}"
275
+ else
276
+ options[:env].inject("env") do |string, (name, value)|
277
+ value = value.to_s.gsub(/[ "]/) { |m| "\\#{m}" }
278
+ string << " #{name}=#{value}"
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,43 @@
1
+ require 'capistrano/logger'
2
+
3
+ require 'capistrano/configuration/callbacks'
4
+ require 'capistrano/configuration/connections'
5
+ require 'capistrano/configuration/execution'
6
+ require 'capistrano/configuration/loading'
7
+ require 'capistrano/configuration/namespaces'
8
+ require 'capistrano/configuration/roles'
9
+ require 'capistrano/configuration/servers'
10
+ require 'capistrano/configuration/variables'
11
+
12
+ require 'capistrano/configuration/actions/file_transfer'
13
+ require 'capistrano/configuration/actions/inspect'
14
+ require 'capistrano/configuration/actions/invocation'
15
+
16
+ module Capistrano
17
+ # Represents a specific Capistrano configuration. A Configuration instance
18
+ # may be used to load multiple recipe files, define and describe tasks,
19
+ # define roles, and set configuration variables.
20
+ class Configuration
21
+ # The logger instance defined for this configuration.
22
+ attr_accessor :debug, :logger, :dry_run
23
+
24
+ def initialize #:nodoc:
25
+ @debug = false
26
+ @dry_run = false
27
+ @logger = Logger.new
28
+ end
29
+
30
+ # make the DSL easier to read when using lazy evaluation via lambdas
31
+ alias defer lambda
32
+
33
+ # The includes must come at the bottom, since they may redefine methods
34
+ # defined in the base class.
35
+ include Connections, Execution, Loading, Namespaces, Roles, Servers, Variables
36
+
37
+ # Mix in the actions
38
+ include Actions::FileTransfer, Actions::Inspect, Actions::Invocation
39
+
40
+ # Must mix last, because it hooks into previously defined methods
41
+ include Callbacks
42
+ end
43
+ end
@@ -0,0 +1,47 @@
1
+ require 'capistrano/transfer'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ module Actions
6
+ module FileTransfer
7
+
8
+ # Store the given data at the given location on all servers targetted
9
+ # by the current task. If <tt>:mode</tt> is specified it is used to
10
+ # set the mode on the file.
11
+ def put(data, path, options={})
12
+ opts = options.dup
13
+ upload(StringIO.new(data), path, opts)
14
+ end
15
+
16
+ # Get file remote_path from FIRST server targeted by
17
+ # the current task and transfer it to local machine as path.
18
+ #
19
+ # get "#{deploy_to}/current/log/production.log", "log/production.log.web"
20
+ def get(remote_path, path, options={}, &block)
21
+ download(remote_path, path, options.merge(:once => true), &block)
22
+ end
23
+
24
+ def upload(from, to, options={}, &block)
25
+ mode = options.delete(:mode)
26
+ transfer(:up, from, to, options, &block)
27
+ if mode
28
+ mode = mode.is_a?(Numeric) ? mode.to_s(8) : mode.to_s
29
+ run "chmod #{mode} #{to}"
30
+ end
31
+ end
32
+
33
+ def download(from, to, options={}, &block)
34
+ transfer(:down, from, to, options, &block)
35
+ end
36
+
37
+ def transfer(direction, from, to, options={}, &block)
38
+ execute_on_servers(options) do |servers|
39
+ targets = servers.map { |s| sessions[s] }
40
+ Transfer.process(direction, from, to, targets, options.merge(:logger => logger), &block)
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ require 'capistrano/errors'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ module Actions
6
+ module Inspect
7
+
8
+ # Streams the result of the command from all servers that are the
9
+ # target of the current task. All these streams will be joined into a
10
+ # single one, so you can, say, watch 10 log files as though they were
11
+ # one. Do note that this is quite expensive from a bandwidth
12
+ # perspective, so use it with care.
13
+ #
14
+ # The command is invoked via #invoke_command.
15
+ #
16
+ # Usage:
17
+ #
18
+ # desc "Run a tail on multiple log files at the same time"
19
+ # task :tail_fcgi, :roles => :app do
20
+ # stream "tail -f #{shared_path}/log/fastcgi.crash.log"
21
+ # end
22
+ def stream(command, options={})
23
+ invoke_command(command, options) do |ch, stream, out|
24
+ puts out if stream == :out
25
+ warn "[err :: #{ch[:server]}] #{out}" if stream == :err
26
+ end
27
+ end
28
+
29
+ # Executes the given command on the first server targetted by the
30
+ # current task, collects it's stdout into a string, and returns the
31
+ # string. The command is invoked via #invoke_command.
32
+ def capture(command, options={})
33
+ output = ""
34
+ invoke_command(command, options.merge(:once => true)) do |ch, stream, data|
35
+ case stream
36
+ when :out then output << data
37
+ when :err then warn "[err :: #{ch[:server]}] #{data}"
38
+ end
39
+ end
40
+ output
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,293 @@
1
+ require 'capistrano/command'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ module Actions
6
+ module Invocation
7
+ def self.included(base) #:nodoc:
8
+ base.extend(ClassMethods)
9
+
10
+ base.send :alias_method, :initialize_without_invocation, :initialize
11
+ base.send :alias_method, :initialize, :initialize_with_invocation
12
+
13
+ base.default_io_proc = Proc.new do |ch, stream, out|
14
+ level = stream == :err ? :important : :info
15
+ ch[:options][:logger].send(level, out, "#{stream} :: #{ch[:server]}")
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+ attr_accessor :default_io_proc
21
+ end
22
+
23
+ def initialize_with_invocation(*args) #:nodoc:
24
+ initialize_without_invocation(*args)
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)
80
+ end
81
+
82
+ # Invokes the given command. If a +via+ key is given, it will be used
83
+ # to determine what method to use to invoke the command. It defaults
84
+ # to :run, but may be :sudo, or any other method that conforms to the
85
+ # same interface as run and sudo.
86
+ def invoke_command(cmd, options={}, &block)
87
+ options = options.dup
88
+ via = options.delete(:via) || :run
89
+ send(via, cmd, options, &block)
90
+ end
91
+
92
+ # Execute the given command on all servers that are the target of the
93
+ # current task. If a block is given, it is invoked for all output
94
+ # generated by the command, and should accept three parameters: the SSH
95
+ # channel (which may be used to send data back to the remote process),
96
+ # the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
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
+ # * :once - if true, only the first matching server will be selected. The default
115
+ # is false (all matching servers will be selected).
116
+ # * :max_hosts - specifies the maximum number of hosts that should be selected
117
+ # at a time. If this value is less than the number of hosts that are selected
118
+ # to run, then the hosts will be run in groups of max_hosts. The default is nil,
119
+ # which indicates that there is no maximum host limit.
120
+ # * :shell - says which shell should be used to invoke commands. This
121
+ # defaults to "sh". Setting this to false causes Capistrano to invoke
122
+ # the commands directly, without wrapping them in a shell invocation.
123
+ # * :data - if not nil (the default), this should be a string that will
124
+ # be passed to the command's stdin stream.
125
+ # * :pty - if true, a pseudo-tty will be allocated for each command. The
126
+ # default is false. Note that there are benefits and drawbacks both ways.
127
+ # Empirically, it appears that if a pty is allocated, the SSH server daemon
128
+ # will _not_ read user shell start-up scripts (e.g. bashrc, etc.). However,
129
+ # if a pty is _not_ allocated, some commands will refuse to run in
130
+ # interactive mode and will not prompt for (e.g.) passwords.
131
+ # * :env - a hash of environment variable mappings that should be made
132
+ # available to the command. The keys should be environment variable names,
133
+ # and the values should be their corresponding values. The default is
134
+ # empty, but may be modified by changing the +default_environment+
135
+ # Capistrano variable.
136
+ #
137
+ # Note that if you set these keys in the +default_run_options+ Capistrano
138
+ # variable, they will apply for all invocations of #run, #invoke_command,
139
+ # and #parallel.
140
+ def run(cmd, options={}, &block)
141
+ block ||= self.class.default_io_proc
142
+ tree = Command::Tree.new(self) { |t| t.else(cmd, &block) }
143
+ run_tree(tree, options)
144
+ end
145
+
146
+ # Executes a Capistrano::Command::Tree object. This is not for direct
147
+ # use, but should instead be called indirectly, via #run or #parallel,
148
+ # or #invoke_command.
149
+ def run_tree(tree, options={}) #:nodoc:
150
+ if tree.branches.empty? && tree.fallback
151
+ logger.debug "executing #{tree.fallback}"
152
+ elsif tree.branches.any?
153
+ logger.debug "executing multiple commands in parallel"
154
+ tree.each do |branch|
155
+ logger.trace "-> #{branch}"
156
+ end
157
+ else
158
+ raise ArgumentError, "attempt to execute without specifying a command"
159
+ end
160
+
161
+ return if dry_run || (debug && continue_execution(tree) == false)
162
+
163
+ options = add_default_command_options(options)
164
+
165
+ tree.each do |branch|
166
+ if branch.command.include?(sudo)
167
+ branch.callback = sudo_behavior_callback(branch.callback)
168
+ end
169
+ end
170
+
171
+ execute_on_servers(options) do |servers|
172
+ targets = servers.map { |s| sessions[s] }
173
+ Command.process(tree, targets, options.merge(:logger => logger))
174
+ end
175
+ end
176
+
177
+ # Returns the command string used by capistrano to invoke a comamnd via
178
+ # sudo.
179
+ #
180
+ # run "#{sudo :as => 'bob'} mkdir /path/to/dir"
181
+ #
182
+ # It can also be invoked like #run, but executing the command via sudo.
183
+ # This assumes that the sudo password (if required) is the same as the
184
+ # password for logging in to the server.
185
+ #
186
+ # sudo "mkdir /path/to/dir"
187
+ #
188
+ # Also, this method understands a <tt>:sudo</tt> configuration variable,
189
+ # which (if specified) will be used as the full path to the sudo
190
+ # executable on the remote machine:
191
+ #
192
+ # set :sudo, "/opt/local/bin/sudo"
193
+ #
194
+ # If you know what you're doing, you can also set <tt>:sudo_prompt</tt>,
195
+ # which tells capistrano which prompt sudo should use when asking for
196
+ # a password. (This is so that capistrano knows what prompt to look for
197
+ # in the output.) If you set :sudo_prompt to an empty string, Capistrano
198
+ # will not send a preferred prompt.
199
+ def sudo(*parameters, &block)
200
+ options = parameters.last.is_a?(Hash) ? parameters.pop.dup : {}
201
+ command = parameters.first
202
+ user = options[:as] && "-u #{options.delete(:as)}"
203
+
204
+ sudo_prompt_option = "-p '#{sudo_prompt}'" unless sudo_prompt.empty?
205
+ sudo_command = [fetch(:sudo, "sudo"), sudo_prompt_option, user].compact.join(" ")
206
+
207
+ if command
208
+ command = sudo_command + " " + command
209
+ run(command, options, &block)
210
+ else
211
+ return sudo_command
212
+ end
213
+ end
214
+
215
+ # Returns a Proc object that defines the behavior of the sudo
216
+ # callback. The returned Proc will defer to the +fallback+ argument
217
+ # (which should also be a Proc) for any output it does not
218
+ # explicitly handle.
219
+ def sudo_behavior_callback(fallback) #:nodoc:
220
+ # in order to prevent _each host_ from prompting when the password
221
+ # was wrong, let's track which host prompted first and only allow
222
+ # subsequent prompts from that host.
223
+ prompt_host = nil
224
+
225
+ Proc.new do |ch, stream, out|
226
+ if out =~ /^Sorry, try again/
227
+ if prompt_host.nil? || prompt_host == ch[:server]
228
+ prompt_host = ch[:server]
229
+ logger.important out, "#{stream} :: #{ch[:server]}"
230
+ reset! :password
231
+ end
232
+ end
233
+
234
+ if out =~ /^#{Regexp.escape(sudo_prompt)}/
235
+ ch.send_data "#{self[:password]}\n"
236
+ elsif fallback
237
+ fallback.call(ch, stream, out)
238
+ end
239
+ end
240
+ end
241
+
242
+ # Merges the various default command options into the options hash and
243
+ # returns the result. The default command options that are understand
244
+ # are:
245
+ #
246
+ # * :default_environment: If the :env key already exists, the :env
247
+ # key is merged into default_environment and then added back into
248
+ # options.
249
+ # * :default_shell: if the :shell key already exists, it will be used.
250
+ # Otherwise, if the :default_shell key exists in the configuration,
251
+ # it will be used. Otherwise, no :shell key is added.
252
+ def add_default_command_options(options)
253
+ defaults = self[:default_run_options]
254
+ options = defaults.merge(options)
255
+
256
+ env = self[:default_environment]
257
+ env = env.merge(options[:env]) if options[:env]
258
+ options[:env] = env unless env.empty?
259
+
260
+ shell = options[:shell] || self[:default_shell]
261
+ options[:shell] = shell unless shell.nil?
262
+
263
+ options
264
+ end
265
+
266
+ # Returns the prompt text to use with sudo
267
+ def sudo_prompt
268
+ fetch(:sudo_prompt, "sudo password: ")
269
+ end
270
+
271
+ def continue_execution(tree)
272
+ if tree.branches.length == 1
273
+ continue_execution_for_branch(tree.branches.first)
274
+ else
275
+ tree.each { |branch| branch.skip! unless continue_execution_for_branch(branch) }
276
+ tree.any? { |branch| !branch.skip? }
277
+ end
278
+ end
279
+
280
+ def continue_execution_for_branch(branch)
281
+ case Capistrano::CLI.debug_prompt(branch)
282
+ when "y"
283
+ true
284
+ when "n"
285
+ false
286
+ when "a"
287
+ exit(-1)
288
+ end
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end