mbailey-capistrano 2.5.5
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/callback.rb +45 -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/cli.rb +47 -0
- data/lib/capistrano/command.rb +283 -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/configuration.rb +43 -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/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/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 +271 -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/scm.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/strategy.rb +19 -0
- data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/recipes/deploy.rb +562 -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/lib/capistrano.rb +2 -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 +205 -0
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
class CLI
|
5
|
+
module Options
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Return a new CLI instance with the given arguments pre-parsed and
|
12
|
+
# ready for execution.
|
13
|
+
def parse(args)
|
14
|
+
cli = new(args)
|
15
|
+
cli.parse_options!
|
16
|
+
cli
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# The hash of (parsed) command-line options
|
21
|
+
attr_reader :options
|
22
|
+
|
23
|
+
# Return an OptionParser instance that defines the acceptable command
|
24
|
+
# line switches for Capistrano, and what their corresponding behaviors
|
25
|
+
# are.
|
26
|
+
def option_parser #:nodoc:
|
27
|
+
@option_parser ||= OptionParser.new do |opts|
|
28
|
+
opts.banner = "Usage: #{File.basename($0)} [options] action ..."
|
29
|
+
|
30
|
+
opts.on("-d", "--debug",
|
31
|
+
"Prompts before each remote command execution."
|
32
|
+
) { |value| options[:debug] = true }
|
33
|
+
|
34
|
+
opts.on("-e", "--explain TASK",
|
35
|
+
"Displays help (if available) for the task."
|
36
|
+
) { |value| options[:explain] = value }
|
37
|
+
|
38
|
+
opts.on("-F", "--default-config",
|
39
|
+
"Always use default config, even with -f."
|
40
|
+
) { options[:default_config] = true }
|
41
|
+
|
42
|
+
opts.on("-f", "--file FILE",
|
43
|
+
"A recipe file to load. May be given more than once."
|
44
|
+
) { |value| options[:recipes] << value }
|
45
|
+
|
46
|
+
opts.on("-H", "--long-help", "Explain these options and environment variables.") do
|
47
|
+
long_help
|
48
|
+
exit
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on("-h", "--help", "Display this help message.") do
|
52
|
+
puts opts
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on("-n", "--dry-run",
|
57
|
+
"Prints out commands without running them."
|
58
|
+
) { |value| options[:dry_run] = true }
|
59
|
+
|
60
|
+
opts.on("-p", "--password",
|
61
|
+
"Immediately prompt for the password."
|
62
|
+
) { options[:password] = nil }
|
63
|
+
|
64
|
+
opts.on("-q", "--quiet",
|
65
|
+
"Make the output as quiet as possible."
|
66
|
+
) { options[:verbose] = 0 }
|
67
|
+
|
68
|
+
opts.on("-S", "--set-before NAME=VALUE",
|
69
|
+
"Set a variable before the recipes are loaded."
|
70
|
+
) do |pair|
|
71
|
+
name, value = pair.split(/=/, 2)
|
72
|
+
options[:pre_vars][name.to_sym] = value
|
73
|
+
end
|
74
|
+
|
75
|
+
opts.on("-s", "--set NAME=VALUE",
|
76
|
+
"Set a variable after the recipes are loaded."
|
77
|
+
) do |pair|
|
78
|
+
name, value = pair.split(/=/, 2)
|
79
|
+
options[:vars][name.to_sym] = value
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on("-T", "--tasks [PATTERN]",
|
83
|
+
"List all tasks (matching optional PATTERN) in the loaded recipe files."
|
84
|
+
) do |value|
|
85
|
+
options[:tasks] = if value
|
86
|
+
value
|
87
|
+
else
|
88
|
+
true
|
89
|
+
end
|
90
|
+
options[:verbose] ||= 0
|
91
|
+
end
|
92
|
+
|
93
|
+
opts.on("-t", "--tool",
|
94
|
+
"Abbreviates the output of -T for tool integration."
|
95
|
+
) { options[:tool] = true }
|
96
|
+
|
97
|
+
opts.on("-V", "--version",
|
98
|
+
"Display the Capistrano version, and exit."
|
99
|
+
) do
|
100
|
+
require 'capistrano/version'
|
101
|
+
puts "Capistrano v#{Capistrano::Version::STRING}"
|
102
|
+
exit
|
103
|
+
end
|
104
|
+
|
105
|
+
opts.on("-v", "--verbose",
|
106
|
+
"Be more verbose. May be given more than once."
|
107
|
+
) do
|
108
|
+
options[:verbose] ||= 0
|
109
|
+
options[:verbose] += 1
|
110
|
+
end
|
111
|
+
|
112
|
+
opts.on("-X", "--skip-system-config",
|
113
|
+
"Don't load the system config file (capistrano.conf)"
|
114
|
+
) { options.delete(:sysconf) }
|
115
|
+
|
116
|
+
opts.on("-x", "--skip-user-config",
|
117
|
+
"Don't load the user config file (.caprc)"
|
118
|
+
) { options.delete(:dotfile) }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# If the arguments to the command are empty, this will print the
|
123
|
+
# allowed options and exit. Otherwise, it will parse the command
|
124
|
+
# line and set up any default options.
|
125
|
+
def parse_options! #:nodoc:
|
126
|
+
@options = { :recipes => [], :actions => [],
|
127
|
+
:vars => {}, :pre_vars => {},
|
128
|
+
:sysconf => default_sysconf, :dotfile => default_dotfile }
|
129
|
+
|
130
|
+
if args.empty?
|
131
|
+
warn "Please specify at least one action to execute."
|
132
|
+
warn option_parser
|
133
|
+
exit
|
134
|
+
end
|
135
|
+
|
136
|
+
option_parser.parse!(args)
|
137
|
+
|
138
|
+
coerce_variable_types!
|
139
|
+
|
140
|
+
# if no verbosity has been specified, be verbose
|
141
|
+
options[:verbose] = 3 if !options.has_key?(:verbose)
|
142
|
+
|
143
|
+
look_for_default_recipe_file! if options[:default_config] || options[:recipes].empty?
|
144
|
+
extract_environment_variables!
|
145
|
+
|
146
|
+
options[:actions].concat(args)
|
147
|
+
|
148
|
+
password = options.has_key?(:password)
|
149
|
+
options[:password] = Proc.new { self.class.password_prompt }
|
150
|
+
options[:password] = options[:password].call if password
|
151
|
+
end
|
152
|
+
|
153
|
+
# Extracts name=value pairs from the remaining command-line arguments
|
154
|
+
# and assigns them as environment variables.
|
155
|
+
def extract_environment_variables! #:nodoc:
|
156
|
+
args.delete_if do |arg|
|
157
|
+
next unless arg.match(/^(\w+)=(.*)$/)
|
158
|
+
ENV[$1] = $2
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Looks for a default recipe file in the current directory.
|
163
|
+
def look_for_default_recipe_file! #:nodoc:
|
164
|
+
current = Dir.pwd
|
165
|
+
|
166
|
+
loop do
|
167
|
+
%w(Capfile capfile).each do |file|
|
168
|
+
if File.file?(file)
|
169
|
+
options[:recipes] << file
|
170
|
+
return
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
pwd = Dir.pwd
|
175
|
+
Dir.chdir("..")
|
176
|
+
break if pwd == Dir.pwd # if changing the directory made no difference, then we're at the top
|
177
|
+
end
|
178
|
+
|
179
|
+
Dir.chdir(current)
|
180
|
+
end
|
181
|
+
|
182
|
+
def default_sysconf #:nodoc:
|
183
|
+
File.join(sysconf_directory, "capistrano.conf")
|
184
|
+
end
|
185
|
+
|
186
|
+
def default_dotfile #:nodoc:
|
187
|
+
File.join(home_directory, ".caprc")
|
188
|
+
end
|
189
|
+
|
190
|
+
def sysconf_directory #:nodoc:
|
191
|
+
# TODO if anyone cares, feel free to submit a patch that uses a more
|
192
|
+
# appropriate location for this file in Windows.
|
193
|
+
ENV["SystemRoot"] || '/etc'
|
194
|
+
end
|
195
|
+
|
196
|
+
def home_directory #:nodoc:
|
197
|
+
ENV["HOME"] ||
|
198
|
+
(ENV["HOMEPATH"] && "#{ENV["HOMEDRIVE"]}#{ENV["HOMEPATH"]}") ||
|
199
|
+
"/"
|
200
|
+
end
|
201
|
+
|
202
|
+
def coerce_variable_types!
|
203
|
+
[:pre_vars, :vars].each do |collection|
|
204
|
+
options[collection].keys.each do |key|
|
205
|
+
options[collection][key] = coerce_variable(options[collection][key])
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def coerce_variable(value)
|
211
|
+
case value
|
212
|
+
when /^"(.*)"$/ then $1
|
213
|
+
when /^'(.*)'$/ then $1
|
214
|
+
when /^\d+$/ then value.to_i
|
215
|
+
when /^\d+\.\d*$/ then value.to_f
|
216
|
+
when "true" then true
|
217
|
+
when "false" then false
|
218
|
+
when "nil" then nil
|
219
|
+
else value
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'highline'
|
2
|
+
|
3
|
+
# work around problem where HighLine detects an eof on $stdin and raises an
|
4
|
+
# error.
|
5
|
+
HighLine.track_eof = false
|
6
|
+
|
7
|
+
module Capistrano
|
8
|
+
class CLI
|
9
|
+
module UI
|
10
|
+
def self.included(base) #:nodoc:
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
# Return the object that provides UI-specific methods, such as prompts
|
16
|
+
# and more.
|
17
|
+
def ui
|
18
|
+
@ui ||= HighLine.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# Prompt for a password using echo suppression.
|
22
|
+
def password_prompt(prompt="Password: ")
|
23
|
+
ui.ask(prompt) { |q| q.echo = false }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Debug mode prompt
|
27
|
+
def debug_prompt(cmd)
|
28
|
+
ui.say("Preparing to execute command: #{cmd}")
|
29
|
+
prompt = "Execute ([Yes], No, Abort) "
|
30
|
+
ui.ask("#{prompt}? ") do |q|
|
31
|
+
q.overwrite = false
|
32
|
+
q.default = 'y'
|
33
|
+
q.validate = /(y(es)?)|(no?)|(a(bort)?|\n)/i
|
34
|
+
q.responses[:not_valid] = prompt
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'capistrano'
|
2
|
+
require 'capistrano/cli/execute'
|
3
|
+
require 'capistrano/cli/help'
|
4
|
+
require 'capistrano/cli/options'
|
5
|
+
require 'capistrano/cli/ui'
|
6
|
+
|
7
|
+
module Capistrano
|
8
|
+
# The CLI class encapsulates the behavior of capistrano when it is invoked
|
9
|
+
# as a command-line utility. This allows other programs to embed Capistrano
|
10
|
+
# and preserve its command-line semantics.
|
11
|
+
class CLI
|
12
|
+
# The array of (unparsed) command-line options
|
13
|
+
attr_reader :args
|
14
|
+
|
15
|
+
# Create a new CLI instance using the given array of command-line parameters
|
16
|
+
# to initialize it. By default, +ARGV+ is used, but you can specify a
|
17
|
+
# different set of parameters (such as when embedded cap in a program):
|
18
|
+
#
|
19
|
+
# require 'capistrano/cli'
|
20
|
+
# Capistrano::CLI.parse(%w(-vvvv -r config/deploy update_code)).execute!
|
21
|
+
#
|
22
|
+
# Note that you can also embed cap directly by creating a new Configuration
|
23
|
+
# instance and setting it up, but you'll often wind up duplicating logic
|
24
|
+
# defined in the CLI class. The above snippet, redone using the Configuration
|
25
|
+
# class directly, would look like:
|
26
|
+
#
|
27
|
+
# require 'capistrano'
|
28
|
+
# require 'capistrano/cli'
|
29
|
+
# config = Capistrano::Configuration.new
|
30
|
+
# config.logger_level = Capistrano::Logger::TRACE
|
31
|
+
# config.set(:password) { Capistrano::CLI.password_prompt }
|
32
|
+
# config.load "config/deploy"
|
33
|
+
# config.update_code
|
34
|
+
#
|
35
|
+
# There may be times that you want/need the additional control offered by
|
36
|
+
# manipulating the Configuration directly, but generally interfacing with
|
37
|
+
# the CLI class is recommended.
|
38
|
+
def initialize(args)
|
39
|
+
@args = args.dup
|
40
|
+
$stdout.sync = true # so that Net::SSH prompts show up
|
41
|
+
end
|
42
|
+
|
43
|
+
# Mix-in the actual behavior
|
44
|
+
include Execute, Options, UI
|
45
|
+
include Help # needs to be included last, because it overrides some methods
|
46
|
+
end
|
47
|
+
end
|
@@ -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,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
|