capistrano 1.4.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +140 -4
- data/MIT-LICENSE +1 -1
- data/README +22 -14
- data/bin/cap +1 -8
- data/bin/capify +77 -0
- data/examples/sample.rb +10 -109
- data/lib/capistrano.rb +1 -0
- data/lib/capistrano/callback.rb +41 -0
- data/lib/capistrano/cli.rb +17 -317
- data/lib/capistrano/cli/execute.rb +82 -0
- data/lib/capistrano/cli/help.rb +102 -0
- data/lib/capistrano/cli/help.txt +53 -0
- data/lib/capistrano/cli/options.rb +183 -0
- data/lib/capistrano/cli/ui.rb +28 -0
- data/lib/capistrano/command.rb +62 -29
- data/lib/capistrano/configuration.rb +25 -226
- data/lib/capistrano/configuration/actions/file_transfer.rb +35 -0
- data/lib/capistrano/configuration/actions/inspect.rb +46 -0
- data/lib/capistrano/configuration/actions/invocation.rb +127 -0
- data/lib/capistrano/configuration/callbacks.rb +148 -0
- data/lib/capistrano/configuration/connections.rb +159 -0
- data/lib/capistrano/configuration/execution.rb +126 -0
- data/lib/capistrano/configuration/loading.rb +112 -0
- data/lib/capistrano/configuration/namespaces.rb +190 -0
- data/lib/capistrano/configuration/roles.rb +51 -0
- data/lib/capistrano/configuration/servers.rb +75 -0
- data/lib/capistrano/configuration/variables.rb +127 -0
- data/lib/capistrano/errors.rb +15 -0
- data/lib/capistrano/extensions.rb +27 -8
- data/lib/capistrano/gateway.rb +54 -29
- data/lib/capistrano/logger.rb +11 -11
- data/lib/capistrano/recipes/compat.rb +32 -0
- data/lib/capistrano/recipes/deploy.rb +483 -0
- data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
- data/lib/capistrano/recipes/deploy/local_dependency.rb +46 -0
- data/lib/capistrano/recipes/deploy/remote_dependency.rb +65 -0
- data/lib/capistrano/recipes/deploy/scm.rb +19 -0
- data/lib/capistrano/recipes/deploy/scm/base.rb +180 -0
- data/lib/capistrano/recipes/deploy/scm/bzr.rb +86 -0
- data/lib/capistrano/recipes/deploy/scm/cvs.rb +151 -0
- data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
- data/lib/capistrano/recipes/deploy/scm/mercurial.rb +129 -0
- data/lib/capistrano/recipes/deploy/scm/perforce.rb +126 -0
- data/lib/capistrano/recipes/deploy/scm/subversion.rb +103 -0
- data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
- data/lib/capistrano/recipes/deploy/strategy/base.rb +64 -0
- data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
- data/lib/capistrano/recipes/deploy/strategy/copy.rb +143 -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 +47 -0
- data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/recipes/standard.rb +26 -276
- data/lib/capistrano/recipes/templates/maintenance.rhtml +1 -1
- data/lib/capistrano/recipes/upgrade.rb +33 -0
- data/lib/capistrano/server_definition.rb +51 -0
- data/lib/capistrano/shell.rb +125 -81
- data/lib/capistrano/ssh.rb +80 -36
- data/lib/capistrano/task_definition.rb +69 -0
- data/lib/capistrano/upload.rb +146 -0
- data/lib/capistrano/version.rb +13 -17
- data/test/cli/execute_test.rb +132 -0
- data/test/cli/help_test.rb +139 -0
- data/test/cli/options_test.rb +226 -0
- data/test/cli/ui_test.rb +28 -0
- data/test/cli_test.rb +17 -0
- data/test/command_test.rb +284 -25
- data/test/configuration/actions/file_transfer_test.rb +40 -0
- data/test/configuration/actions/inspect_test.rb +62 -0
- data/test/configuration/actions/invocation_test.rb +195 -0
- data/test/configuration/callbacks_test.rb +206 -0
- data/test/configuration/connections_test.rb +288 -0
- data/test/configuration/execution_test.rb +159 -0
- data/test/configuration/loading_test.rb +119 -0
- data/test/configuration/namespace_dsl_test.rb +283 -0
- data/test/configuration/roles_test.rb +47 -0
- data/test/configuration/servers_test.rb +90 -0
- data/test/configuration/variables_test.rb +180 -0
- data/test/configuration_test.rb +60 -212
- data/test/deploy/scm/base_test.rb +55 -0
- data/test/deploy/strategy/copy_test.rb +146 -0
- data/test/extensions_test.rb +69 -0
- data/test/fixtures/cli_integration.rb +5 -0
- data/test/fixtures/custom.rb +2 -2
- data/test/gateway_test.rb +167 -0
- data/test/logger_test.rb +123 -0
- data/test/server_definition_test.rb +108 -0
- data/test/shell_test.rb +64 -0
- data/test/ssh_test.rb +67 -154
- data/test/task_definition_test.rb +101 -0
- data/test/upload_test.rb +131 -0
- data/test/utils.rb +31 -39
- data/test/version_test.rb +24 -0
- metadata +145 -98
- data/THANKS +0 -4
- data/lib/capistrano/actor.rb +0 -567
- data/lib/capistrano/generators/rails/deployment/deployment_generator.rb +0 -25
- data/lib/capistrano/generators/rails/deployment/templates/capistrano.rake +0 -49
- data/lib/capistrano/generators/rails/deployment/templates/deploy.rb +0 -122
- data/lib/capistrano/generators/rails/loader.rb +0 -20
- data/lib/capistrano/scm/base.rb +0 -61
- data/lib/capistrano/scm/baz.rb +0 -118
- data/lib/capistrano/scm/bzr.rb +0 -70
- data/lib/capistrano/scm/cvs.rb +0 -129
- data/lib/capistrano/scm/darcs.rb +0 -27
- data/lib/capistrano/scm/mercurial.rb +0 -83
- data/lib/capistrano/scm/perforce.rb +0 -139
- data/lib/capistrano/scm/subversion.rb +0 -128
- data/lib/capistrano/transfer.rb +0 -97
- data/lib/capistrano/utils.rb +0 -26
- data/test/actor_test.rb +0 -402
- data/test/scm/cvs_test.rb +0 -196
- data/test/scm/subversion_test.rb +0 -145
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'capistrano/upload'
|
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
|
+
execute_on_servers(options) do |servers|
|
13
|
+
targets = servers.map { |s| sessions[s] }
|
14
|
+
Upload.process(targets, path, :data => data, :mode => options[:mode], :logger => logger)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get file remote_path from FIRST server targetted by
|
19
|
+
# the current task and transfer it to local machine as path.
|
20
|
+
#
|
21
|
+
# get "#{deploy_to}/current/log/production.log", "log/production.log.web"
|
22
|
+
def get(remote_path, path, options = {})
|
23
|
+
execute_on_servers(options.merge(:once => true)) do |servers|
|
24
|
+
logger.info "downloading `#{servers.first.host}:#{remote_path}' to `#{path}'"
|
25
|
+
sftp = sessions[servers.first].sftp
|
26
|
+
sftp.connect unless sftp.state == :open
|
27
|
+
sftp.get_file remote_path, path
|
28
|
+
logger.debug "download finished"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
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 raise CaptureError, "error processing #{command.inspect}: #{data.inspect}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
output
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,127 @@
|
|
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
|
+
end
|
27
|
+
|
28
|
+
# Invokes the given command. If a +via+ key is given, it will be used
|
29
|
+
# to determine what method to use to invoke the command. It defaults
|
30
|
+
# to :run, but may be :sudo, or any other method that conforms to the
|
31
|
+
# same interface as run and sudo.
|
32
|
+
def invoke_command(cmd, options={}, &block)
|
33
|
+
options = options.dup
|
34
|
+
via = options.delete(:via) || :run
|
35
|
+
send(via, cmd, options, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Execute the given command on all servers that are the target of the
|
39
|
+
# current task. If a block is given, it is invoked for all output
|
40
|
+
# generated by the command, and should accept three parameters: the SSH
|
41
|
+
# channel (which may be used to send data back to the remote process),
|
42
|
+
# the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
|
43
|
+
# stdout), and the data that was received.
|
44
|
+
def run(cmd, options={}, &block)
|
45
|
+
block ||= self.class.default_io_proc
|
46
|
+
logger.debug "executing #{cmd.strip.inspect}"
|
47
|
+
|
48
|
+
options = add_default_command_options(options)
|
49
|
+
|
50
|
+
execute_on_servers(options) do |servers|
|
51
|
+
targets = servers.map { |s| sessions[s] }
|
52
|
+
Command.process(cmd, targets, options.merge(:logger => logger), &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Like #run, but executes the command via <tt>sudo</tt>. This assumes
|
57
|
+
# that the sudo password (if required) is the same as the password for
|
58
|
+
# logging in to the server.
|
59
|
+
#
|
60
|
+
# Also, this module accepts a <tt>:sudo</tt> configuration variable,
|
61
|
+
# which (if specified) will be used as the full path to the sudo
|
62
|
+
# executable on the remote machine:
|
63
|
+
#
|
64
|
+
# set :sudo, "/opt/local/bin/sudo"
|
65
|
+
def sudo(command, options={}, &block)
|
66
|
+
block ||= self.class.default_io_proc
|
67
|
+
|
68
|
+
options = options.dup
|
69
|
+
as = options.delete(:as)
|
70
|
+
|
71
|
+
user = as && "-u #{as}"
|
72
|
+
command = [fetch(:sudo, "sudo"), user, command].compact.join(" ")
|
73
|
+
|
74
|
+
run(command, options, &sudo_behavior_callback(block))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns a Proc object that defines the behavior of the sudo
|
78
|
+
# callback. The returned Proc will defer to the +fallback+ argument
|
79
|
+
# (which should also be a Proc) for any output it does not
|
80
|
+
# explicitly handle.
|
81
|
+
def sudo_behavior_callback(fallback) #:nodoc:
|
82
|
+
# in order to prevent _each host_ from prompting when the password
|
83
|
+
# was wrong, let's track which host prompted first and only allow
|
84
|
+
# subsequent prompts from that host.
|
85
|
+
prompt_host = nil
|
86
|
+
|
87
|
+
Proc.new do |ch, stream, out|
|
88
|
+
if out =~ /password:/i
|
89
|
+
ch.send_data "#{self[:password]}\n"
|
90
|
+
elsif out =~ /try again/
|
91
|
+
if prompt_host.nil? || prompt_host == ch[:server]
|
92
|
+
prompt_host = ch[:server]
|
93
|
+
logger.important out, "#{stream} :: #{ch[:server]}"
|
94
|
+
reset! :password
|
95
|
+
end
|
96
|
+
else
|
97
|
+
fallback.call(ch, stream, out)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Merges the various default command options into the options hash and
|
103
|
+
# returns the result. The default command options that are understand
|
104
|
+
# are:
|
105
|
+
#
|
106
|
+
# * :default_environment: If the :env key already exists, the :env
|
107
|
+
# key is merged into default_environment and then added back into
|
108
|
+
# options.
|
109
|
+
# * :default_shell: if the :shell key already exists, it will be used.
|
110
|
+
# Otherwise, if the :default_shell key exists in the configuration,
|
111
|
+
# it will be used. Otherwise, no :shell key is added.
|
112
|
+
def add_default_command_options(options)
|
113
|
+
options = options.dup
|
114
|
+
|
115
|
+
env = self[:default_environment]
|
116
|
+
env = env.merge(options[:env]) if options[:env]
|
117
|
+
options[:env] = env unless env.empty?
|
118
|
+
|
119
|
+
shell = options[:shell] || self[:default_shell]
|
120
|
+
options[:shell] = shell if shell
|
121
|
+
|
122
|
+
options
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'capistrano/callback'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
class Configuration
|
5
|
+
module Callbacks
|
6
|
+
def self.included(base) #:nodoc:
|
7
|
+
%w(initialize execute_task).each do |method|
|
8
|
+
base.send :alias_method, "#{method}_without_callbacks", method
|
9
|
+
base.send :alias_method, method, "#{method}_with_callbacks"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# The hash of callbacks that have been registered for this configuration
|
14
|
+
attr_reader :callbacks
|
15
|
+
|
16
|
+
def initialize_with_callbacks(*args) #:nodoc:
|
17
|
+
initialize_without_callbacks(*args)
|
18
|
+
@callbacks = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def execute_task_with_callbacks(task) #:nodoc:
|
22
|
+
before = find_hook(task, :before)
|
23
|
+
execute_task(before) if before
|
24
|
+
|
25
|
+
trigger :before, task
|
26
|
+
|
27
|
+
result = execute_task_without_callbacks(task)
|
28
|
+
|
29
|
+
trigger :after, task
|
30
|
+
|
31
|
+
after = find_hook(task, :after)
|
32
|
+
execute_task(after) if after
|
33
|
+
|
34
|
+
return result
|
35
|
+
end
|
36
|
+
|
37
|
+
# Defines a callback to be invoked before the given task. You must
|
38
|
+
# specify the fully-qualified task name, both for the primary task, and
|
39
|
+
# for the task(s) to be executed before. Alternatively, you can pass a
|
40
|
+
# block to be executed before the given task.
|
41
|
+
#
|
42
|
+
# before "deploy:update_code", :record_difference
|
43
|
+
# before :deploy, "custom:log_deploy"
|
44
|
+
# before :deploy, :this, "then:this", "and:then:this"
|
45
|
+
# before :some_task do
|
46
|
+
# puts "an anonymous hook!"
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# This just provides a convenient interface to the more general #on method.
|
50
|
+
def before(task_name, *args, &block)
|
51
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
52
|
+
args << options.merge(:only => task_name)
|
53
|
+
on :before, *args, &block
|
54
|
+
end
|
55
|
+
|
56
|
+
# Defines a callback to be invoked after the given task. You must
|
57
|
+
# specify the fully-qualified task name, both for the primary task, and
|
58
|
+
# for the task(s) to be executed after. Alternatively, you can pass a
|
59
|
+
# block to be executed after the given task.
|
60
|
+
#
|
61
|
+
# after "deploy:update_code", :log_difference
|
62
|
+
# after :deploy, "custom:announce"
|
63
|
+
# after :deploy, :this, "then:this", "and:then:this"
|
64
|
+
# after :some_task do
|
65
|
+
# puts "an anonymous hook!"
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# This just provides a convenient interface to the more general #on method.
|
69
|
+
def after(task_name, *args, &block)
|
70
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
71
|
+
args << options.merge(:only => task_name)
|
72
|
+
on :after, *args, &block
|
73
|
+
end
|
74
|
+
|
75
|
+
# Defines one or more callbacks to be invoked in response to some event.
|
76
|
+
# Capistrano currently understands the following events:
|
77
|
+
#
|
78
|
+
# * :before, triggered before a task is invoked
|
79
|
+
# * :after, triggered after a task is invoked
|
80
|
+
# * :start, triggered before a top-level task is invoked via the command-line
|
81
|
+
# * :finish, triggered when a top-level task completes
|
82
|
+
# * :load, triggered after all recipes have loaded
|
83
|
+
# * :exit, triggered after all tasks have completed
|
84
|
+
#
|
85
|
+
# Specify the (fully-qualified) task names that you want invoked in
|
86
|
+
# response to the event. Alternatively, you can specify a block to invoke
|
87
|
+
# when the event is triggered. You can also pass a hash of options as the
|
88
|
+
# last parameter, which may include either of two keys:
|
89
|
+
#
|
90
|
+
# * :only, should specify an array of task names. Restricts this callback
|
91
|
+
# so that it will only fire when the event applies to those tasks.
|
92
|
+
# * :except, should specify an array of task names. Restricts this callback
|
93
|
+
# so that it will never fire when the event applies to those tasks.
|
94
|
+
#
|
95
|
+
# Usage:
|
96
|
+
#
|
97
|
+
# on :before, "some:hook", "another:hook", :only => "deploy:update"
|
98
|
+
# on :after, "some:hook", :except => "deploy:symlink"
|
99
|
+
# on :before, "global:hook"
|
100
|
+
# on :after, :only => :deploy do
|
101
|
+
# puts "after deploy here"
|
102
|
+
# end
|
103
|
+
def on(event, *args, &block)
|
104
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
105
|
+
callbacks[event] ||= []
|
106
|
+
|
107
|
+
if args.empty? && block.nil?
|
108
|
+
raise ArgumentError, "please specify either a task name or a block to invoke"
|
109
|
+
elsif args.any? && block
|
110
|
+
raise ArgumentError, "please specify only a task name or a block, but not both"
|
111
|
+
elsif block
|
112
|
+
callbacks[event] << ProcCallback.new(block, options)
|
113
|
+
else
|
114
|
+
args.each do |name|
|
115
|
+
callbacks[event] << TaskCallback.new(self, name, options)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Trigger the named event for the named task. All associated callbacks
|
121
|
+
# will be fired, in the order they were defined.
|
122
|
+
def trigger(event, task=nil)
|
123
|
+
pending = Array(callbacks[event]).select { |c| c.applies_to?(task) }
|
124
|
+
if pending.any?
|
125
|
+
msg = "triggering #{event} callbacks"
|
126
|
+
msg << " for `#{task.fully_qualified_name}'" if task
|
127
|
+
logger.trace(msg)
|
128
|
+
pending.each { |callback| callback.call }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
# Looks for before_foo or after_foo tasks. This method of extending tasks
|
135
|
+
# is now discouraged (though not formally deprecated). You should use the
|
136
|
+
# before and after methods to declare hooks for such callbacks.
|
137
|
+
def find_hook(task, hook)
|
138
|
+
if task == task.namespace.default_task
|
139
|
+
result = task.namespace.search_task("#{hook}_#{task.namespace.name}")
|
140
|
+
return result if result
|
141
|
+
end
|
142
|
+
|
143
|
+
task.namespace.search_task("#{hook}_#{task.name}")
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'capistrano/gateway'
|
2
|
+
require 'capistrano/ssh'
|
3
|
+
|
4
|
+
module Capistrano
|
5
|
+
class Configuration
|
6
|
+
module Connections
|
7
|
+
def self.included(base) #:nodoc:
|
8
|
+
base.send :alias_method, :initialize_without_connections, :initialize
|
9
|
+
base.send :alias_method, :initialize, :initialize_with_connections
|
10
|
+
end
|
11
|
+
|
12
|
+
# An adaptor for making the SSH interface look and act like that of the
|
13
|
+
# Gateway class.
|
14
|
+
class DefaultConnectionFactory #:nodoc:
|
15
|
+
def initialize(options)
|
16
|
+
@options = options
|
17
|
+
end
|
18
|
+
|
19
|
+
def connect_to(server)
|
20
|
+
SSH.connect(server, @options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# A hash of the SSH sessions that are currently open and available.
|
25
|
+
# Because sessions are constructed lazily, this will only contain
|
26
|
+
# connections to those servers that have been the targets of one or more
|
27
|
+
# executed tasks.
|
28
|
+
attr_reader :sessions
|
29
|
+
|
30
|
+
def initialize_with_connections(*args) #:nodoc:
|
31
|
+
initialize_without_connections(*args)
|
32
|
+
@sessions = {}
|
33
|
+
@failed_sessions = []
|
34
|
+
end
|
35
|
+
|
36
|
+
# Indicate that the given server could not be connected to.
|
37
|
+
def failed!(server)
|
38
|
+
@failed_sessions << server
|
39
|
+
end
|
40
|
+
|
41
|
+
# Query whether previous connection attempts to the given server have
|
42
|
+
# failed.
|
43
|
+
def has_failed?(server)
|
44
|
+
@failed_sessions.include?(server)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Used to force connections to be made to the current task's servers.
|
48
|
+
# Connections are normally made lazily in Capistrano--you can use this
|
49
|
+
# to force them open before performing some operation that might be
|
50
|
+
# time-sensitive.
|
51
|
+
def connect!(options={})
|
52
|
+
execute_on_servers(options) { }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the object responsible for establishing new SSH connections.
|
56
|
+
# The factory will respond to #connect_to, which can be used to
|
57
|
+
# establish connections to servers defined via ServerDefinition objects.
|
58
|
+
def connection_factory
|
59
|
+
@connection_factory ||= begin
|
60
|
+
if exists?(:gateway)
|
61
|
+
logger.debug "establishing connection to gateway `#{fetch(:gateway)}'"
|
62
|
+
Gateway.new(ServerDefinition.new(fetch(:gateway)), self)
|
63
|
+
else
|
64
|
+
DefaultConnectionFactory.new(self)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Ensures that there are active sessions for each server in the list.
|
70
|
+
def establish_connections_to(servers)
|
71
|
+
failed_servers = []
|
72
|
+
|
73
|
+
# This attemps to work around the problem where SFTP uploads hang
|
74
|
+
# for some people. A bit of investigating seemed to reveal that the
|
75
|
+
# hang only occurred when the SSH connections were established async,
|
76
|
+
# so this setting allows people to at least work around the problem.
|
77
|
+
if fetch(:synchronous_connect, false)
|
78
|
+
logger.trace "synchronous_connect: true"
|
79
|
+
Array(servers).each { |server| safely_establish_connection_to(server, failed_servers) }
|
80
|
+
else
|
81
|
+
# force the connection factory to be instantiated synchronously,
|
82
|
+
# otherwise we wind up with multiple gateway instances, because
|
83
|
+
# each connection is done in parallel.
|
84
|
+
connection_factory
|
85
|
+
|
86
|
+
threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
|
87
|
+
threads.each { |t| t.join }
|
88
|
+
end
|
89
|
+
|
90
|
+
if failed_servers.any?
|
91
|
+
errors = failed_servers.map { |h| "#{h[:server]} (#{h[:error].class}: #{h[:error].message})" }
|
92
|
+
error = ConnectionError.new("connection failed for: #{errors.join(', ')}")
|
93
|
+
error.hosts = failed_servers.map { |h| h[:server] }
|
94
|
+
raise error
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Determines the set of servers within the current task's scope and
|
99
|
+
# establishes connections to them, and then yields that list of
|
100
|
+
# servers.
|
101
|
+
def execute_on_servers(options={})
|
102
|
+
raise ArgumentError, "expected a block" unless block_given?
|
103
|
+
|
104
|
+
if task = current_task
|
105
|
+
servers = find_servers_for_task(task, options)
|
106
|
+
|
107
|
+
if servers.empty?
|
108
|
+
raise Capistrano::NoMatchingServersError, "`#{task.fully_qualified_name}' is only run for servers matching #{task.options.inspect}, but no servers matched"
|
109
|
+
end
|
110
|
+
|
111
|
+
if task.continue_on_error?
|
112
|
+
servers.delete_if { |s| has_failed?(s) }
|
113
|
+
return if servers.empty?
|
114
|
+
end
|
115
|
+
else
|
116
|
+
servers = find_servers(options)
|
117
|
+
raise Capistrano::NoMatchingServersError, "no servers found to match #{options.inspect}" if servers.empty?
|
118
|
+
end
|
119
|
+
|
120
|
+
servers = [servers.first] if options[:once]
|
121
|
+
logger.trace "servers: #{servers.map { |s| s.host }.inspect}"
|
122
|
+
|
123
|
+
# establish connections to those servers, as necessary
|
124
|
+
begin
|
125
|
+
establish_connections_to(servers)
|
126
|
+
rescue ConnectionError => error
|
127
|
+
raise error unless task && task.continue_on_error?
|
128
|
+
error.hosts.each do |h|
|
129
|
+
servers.delete(h)
|
130
|
+
failed!(h)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
begin
|
135
|
+
yield servers
|
136
|
+
rescue RemoteError => error
|
137
|
+
raise error unless task && task.continue_on_error?
|
138
|
+
error.hosts.each { |h| failed!(h) }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# We establish the connection by creating a thread in a new method--this
|
145
|
+
# prevents problems with the thread's scope seeing the wrong 'server'
|
146
|
+
# variable if the thread just happens to take too long to start up.
|
147
|
+
def establish_connection_to(server, failures=nil)
|
148
|
+
Thread.new { safely_establish_connection_to(server, failures) }
|
149
|
+
end
|
150
|
+
|
151
|
+
def safely_establish_connection_to(server, failures=nil)
|
152
|
+
sessions[server] ||= connection_factory.connect_to(server)
|
153
|
+
rescue Exception => err
|
154
|
+
raise unless failures
|
155
|
+
failures << { :server => server, :error => err }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|