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.
- data/CHANGELOG.rdoc +761 -0
- data/Manifest +104 -0
- data/README.rdoc +66 -0
- data/Rakefile +34 -0
- data/bin/cap +4 -0
- data/bin/capify +78 -0
- data/examples/sample.rb +14 -0
- data/lib/capistrano.rb +2 -0
- data/lib/capistrano/callback.rb +45 -0
- data/lib/capistrano/cli.rb +47 -0
- data/lib/capistrano/cli/execute.rb +84 -0
- data/lib/capistrano/cli/help.rb +125 -0
- data/lib/capistrano/cli/help.txt +75 -0
- data/lib/capistrano/cli/options.rb +224 -0
- data/lib/capistrano/cli/ui.rb +40 -0
- data/lib/capistrano/command.rb +283 -0
- data/lib/capistrano/configuration.rb +43 -0
- data/lib/capistrano/configuration/actions/file_transfer.rb +47 -0
- data/lib/capistrano/configuration/actions/inspect.rb +46 -0
- data/lib/capistrano/configuration/actions/invocation.rb +293 -0
- data/lib/capistrano/configuration/callbacks.rb +148 -0
- data/lib/capistrano/configuration/connections.rb +200 -0
- data/lib/capistrano/configuration/execution.rb +132 -0
- data/lib/capistrano/configuration/loading.rb +197 -0
- data/lib/capistrano/configuration/namespaces.rb +197 -0
- data/lib/capistrano/configuration/roles.rb +73 -0
- data/lib/capistrano/configuration/servers.rb +85 -0
- data/lib/capistrano/configuration/variables.rb +127 -0
- data/lib/capistrano/errors.rb +15 -0
- data/lib/capistrano/extensions.rb +57 -0
- data/lib/capistrano/logger.rb +59 -0
- data/lib/capistrano/processable.rb +53 -0
- data/lib/capistrano/recipes/compat.rb +32 -0
- data/lib/capistrano/recipes/deploy.rb +562 -0
- data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
- data/lib/capistrano/recipes/deploy/local_dependency.rb +54 -0
- data/lib/capistrano/recipes/deploy/remote_dependency.rb +105 -0
- data/lib/capistrano/recipes/deploy/scm.rb +19 -0
- data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
- data/lib/capistrano/recipes/deploy/scm/base.rb +196 -0
- data/lib/capistrano/recipes/deploy/scm/bzr.rb +83 -0
- data/lib/capistrano/recipes/deploy/scm/cvs.rb +152 -0
- data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
- data/lib/capistrano/recipes/deploy/scm/git.rb +302 -0
- data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
- data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
- data/lib/capistrano/recipes/deploy/scm/perforce.rb +133 -0
- data/lib/capistrano/recipes/deploy/scm/subversion.rb +121 -0
- data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
- data/lib/capistrano/recipes/deploy/strategy/base.rb +79 -0
- data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
- data/lib/capistrano/recipes/deploy/strategy/copy.rb +210 -0
- data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
- data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
- data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +56 -0
- data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/recipes/standard.rb +37 -0
- data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/recipes/upgrade.rb +33 -0
- data/lib/capistrano/role.rb +102 -0
- data/lib/capistrano/server_definition.rb +56 -0
- data/lib/capistrano/shell.rb +260 -0
- data/lib/capistrano/ssh.rb +99 -0
- data/lib/capistrano/task_definition.rb +70 -0
- data/lib/capistrano/transfer.rb +216 -0
- data/lib/capistrano/version.rb +18 -0
- data/setup.rb +1346 -0
- data/test/cli/execute_test.rb +132 -0
- data/test/cli/help_test.rb +165 -0
- data/test/cli/options_test.rb +317 -0
- data/test/cli/ui_test.rb +28 -0
- data/test/cli_test.rb +17 -0
- data/test/command_test.rb +286 -0
- data/test/configuration/actions/file_transfer_test.rb +61 -0
- data/test/configuration/actions/inspect_test.rb +65 -0
- data/test/configuration/actions/invocation_test.rb +224 -0
- data/test/configuration/callbacks_test.rb +220 -0
- data/test/configuration/connections_test.rb +349 -0
- data/test/configuration/execution_test.rb +175 -0
- data/test/configuration/loading_test.rb +132 -0
- data/test/configuration/namespace_dsl_test.rb +311 -0
- data/test/configuration/roles_test.rb +144 -0
- data/test/configuration/servers_test.rb +121 -0
- data/test/configuration/variables_test.rb +184 -0
- data/test/configuration_test.rb +88 -0
- data/test/deploy/local_dependency_test.rb +76 -0
- data/test/deploy/remote_dependency_test.rb +114 -0
- data/test/deploy/scm/accurev_test.rb +23 -0
- data/test/deploy/scm/base_test.rb +55 -0
- data/test/deploy/scm/git_test.rb +167 -0
- data/test/deploy/scm/mercurial_test.rb +129 -0
- data/test/deploy/strategy/copy_test.rb +258 -0
- data/test/extensions_test.rb +69 -0
- data/test/fixtures/cli_integration.rb +5 -0
- data/test/fixtures/config.rb +5 -0
- data/test/fixtures/custom.rb +3 -0
- data/test/logger_test.rb +123 -0
- data/test/role_test.rb +11 -0
- data/test/server_definition_test.rb +121 -0
- data/test/shell_test.rb +90 -0
- data/test/ssh_test.rb +104 -0
- data/test/task_definition_test.rb +101 -0
- data/test/transfer_test.rb +160 -0
- data/test/utils.rb +38 -0
- 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
|