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,102 @@
|
|
1
|
+
module Capistrano
|
2
|
+
class Role
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize(*list)
|
6
|
+
@static_servers = []
|
7
|
+
@dynamic_servers = []
|
8
|
+
push(*list)
|
9
|
+
end
|
10
|
+
|
11
|
+
def each(&block)
|
12
|
+
servers.each &block
|
13
|
+
end
|
14
|
+
|
15
|
+
def push(*list)
|
16
|
+
options = list.last.is_a?(Hash) ? list.pop : {}
|
17
|
+
list.each do |item|
|
18
|
+
if item.respond_to?(:call)
|
19
|
+
@dynamic_servers << DynamicServerList.new(item, options)
|
20
|
+
else
|
21
|
+
@static_servers << self.class.wrap_server(item, options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
alias_method :<<, :push
|
26
|
+
|
27
|
+
def servers
|
28
|
+
@static_servers + dynamic_servers
|
29
|
+
end
|
30
|
+
alias_method :to_ary, :servers
|
31
|
+
|
32
|
+
def empty?
|
33
|
+
servers.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear
|
37
|
+
@dynamic_servers.clear
|
38
|
+
@static_servers.clear
|
39
|
+
end
|
40
|
+
|
41
|
+
def include?(server)
|
42
|
+
servers.include?(server)
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
# This is the combination of a block, a hash of options, and a cached value.
|
48
|
+
class DynamicServerList
|
49
|
+
def initialize (block, options)
|
50
|
+
@block = block
|
51
|
+
@options = options
|
52
|
+
@cached = []
|
53
|
+
@is_cached = false
|
54
|
+
end
|
55
|
+
|
56
|
+
# Convert to a list of ServerDefinitions
|
57
|
+
def to_ary
|
58
|
+
unless @is_cached
|
59
|
+
@cached = Role::wrap_list(@block.call(@options), @options)
|
60
|
+
@is_cached = true
|
61
|
+
end
|
62
|
+
@cached
|
63
|
+
end
|
64
|
+
|
65
|
+
# Clear the cached value
|
66
|
+
def reset!
|
67
|
+
@cached.clear
|
68
|
+
@is_cached = false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Attribute reader for the cached results of executing the blocks in turn
|
73
|
+
def dynamic_servers
|
74
|
+
@dynamic_servers.inject([]) { |list, item| list.concat item }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Wraps a string in a ServerDefinition, if it isn't already.
|
78
|
+
# This and wrap_list should probably go in ServerDefinition in some form.
|
79
|
+
def self.wrap_server (item, options)
|
80
|
+
item.is_a?(ServerDefinition) ? item : ServerDefinition.new(item, options)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Turns a list, or something resembling a list, into a properly-formatted
|
84
|
+
# ServerDefinition list. Keep an eye on this one -- it's entirely too
|
85
|
+
# magical for its own good. In particular, if ServerDefinition ever inherits
|
86
|
+
# from Array, this will break.
|
87
|
+
def self.wrap_list (*list)
|
88
|
+
options = list.last.is_a?(Hash) ? list.pop : {}
|
89
|
+
if list.length == 1
|
90
|
+
if list.first.nil?
|
91
|
+
return []
|
92
|
+
elsif list.first.is_a?(Array)
|
93
|
+
list = list.first
|
94
|
+
end
|
95
|
+
end
|
96
|
+
options.merge! list.pop if list.last.is_a?(Hash)
|
97
|
+
list.map do |item|
|
98
|
+
self.wrap_server item, options
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Capistrano
|
2
|
+
class ServerDefinition
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
attr_reader :host
|
6
|
+
attr_reader :user
|
7
|
+
attr_reader :port
|
8
|
+
attr_reader :options
|
9
|
+
|
10
|
+
# The default user name to use when a user name is not explicitly provided
|
11
|
+
def self.default_user
|
12
|
+
ENV['USER'] || ENV['USERNAME'] || "not-specified"
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(string, options={})
|
16
|
+
@user, @host, @port = string.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3]
|
17
|
+
|
18
|
+
@options = options.dup
|
19
|
+
user_opt, port_opt = @options.delete(:user), @options.delete(:port)
|
20
|
+
|
21
|
+
@user ||= user_opt
|
22
|
+
@port ||= port_opt
|
23
|
+
|
24
|
+
@port = @port.to_i if @port
|
25
|
+
end
|
26
|
+
|
27
|
+
def <=>(server)
|
28
|
+
[host, port, user] <=> [server.host, server.port, server.user]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Redefined, so that Array#uniq will work to remove duplicate server
|
32
|
+
# definitions, based solely on their host names.
|
33
|
+
def eql?(server)
|
34
|
+
host == server.host &&
|
35
|
+
user == server.user &&
|
36
|
+
port == server.port
|
37
|
+
end
|
38
|
+
|
39
|
+
alias :== :eql?
|
40
|
+
|
41
|
+
# Redefined, so that Array#uniq will work to remove duplicate server
|
42
|
+
# definitions, based on their connection information.
|
43
|
+
def hash
|
44
|
+
@hash ||= [host, user, port].hash
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
@to_s ||= begin
|
49
|
+
s = host
|
50
|
+
s = "#{user}@#{s}" if user
|
51
|
+
s = "#{s}:#{port}" if port && port != 22
|
52
|
+
s
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'capistrano/processable'
|
3
|
+
|
4
|
+
module Capistrano
|
5
|
+
# The Capistrano::Shell class is the guts of the "shell" task. It implements
|
6
|
+
# an interactive REPL interface that users can employ to execute tasks and
|
7
|
+
# commands. It makes for a GREAT way to monitor systems, and perform quick
|
8
|
+
# maintenance on one or more machines.
|
9
|
+
class Shell
|
10
|
+
include Processable
|
11
|
+
|
12
|
+
# A Readline replacement for platforms where readline is either
|
13
|
+
# unavailable, or has not been installed.
|
14
|
+
class ReadlineFallback #:nodoc:
|
15
|
+
HISTORY = []
|
16
|
+
|
17
|
+
def self.readline(prompt)
|
18
|
+
STDOUT.print(prompt)
|
19
|
+
STDOUT.flush
|
20
|
+
STDIN.gets
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# The configuration instance employed by this shell
|
25
|
+
attr_reader :configuration
|
26
|
+
|
27
|
+
# Instantiate a new shell and begin executing it immediately.
|
28
|
+
def self.run(config)
|
29
|
+
new(config).run!
|
30
|
+
end
|
31
|
+
|
32
|
+
# Instantiate a new shell
|
33
|
+
def initialize(config)
|
34
|
+
@configuration = config
|
35
|
+
end
|
36
|
+
|
37
|
+
# Start the shell running. This method will block until the shell
|
38
|
+
# terminates.
|
39
|
+
def run!
|
40
|
+
setup
|
41
|
+
|
42
|
+
puts <<-INTRO
|
43
|
+
====================================================================
|
44
|
+
Welcome to the interactive Capistrano shell! This is an experimental
|
45
|
+
feature, and is liable to change in future releases. Type 'help' for
|
46
|
+
a summary of how to use the shell.
|
47
|
+
--------------------------------------------------------------------
|
48
|
+
INTRO
|
49
|
+
|
50
|
+
loop do
|
51
|
+
break if !read_and_execute
|
52
|
+
end
|
53
|
+
|
54
|
+
@bgthread.kill
|
55
|
+
end
|
56
|
+
|
57
|
+
def read_and_execute
|
58
|
+
command = read_line
|
59
|
+
|
60
|
+
case command
|
61
|
+
when "?", "help" then help
|
62
|
+
when "quit", "exit" then
|
63
|
+
puts "exiting"
|
64
|
+
return false
|
65
|
+
when /^set -(\w)\s*(\S+)/
|
66
|
+
set_option($1, $2)
|
67
|
+
when /^(?:(with|on)\s*(\S+))?\s*(\S.*)?/i
|
68
|
+
process_command($1, $2, $3)
|
69
|
+
else
|
70
|
+
raise "eh?"
|
71
|
+
end
|
72
|
+
|
73
|
+
return true
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# Present the prompt and read a single line from the console. It also
|
79
|
+
# detects ^D and returns "exit" in that case. Adds the input to the
|
80
|
+
# history, unless the input is empty. Loops repeatedly until a non-empty
|
81
|
+
# line is input.
|
82
|
+
def read_line
|
83
|
+
loop do
|
84
|
+
command = reader.readline("cap> ")
|
85
|
+
|
86
|
+
if command.nil?
|
87
|
+
command = "exit"
|
88
|
+
puts(command)
|
89
|
+
else
|
90
|
+
command.strip!
|
91
|
+
end
|
92
|
+
|
93
|
+
unless command.empty?
|
94
|
+
reader::HISTORY << command
|
95
|
+
return command
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Display a verbose help message.
|
101
|
+
def help
|
102
|
+
puts <<-HELP
|
103
|
+
--- HELP! ---------------------------------------------------
|
104
|
+
"Get me out of this thing. I just want to quit."
|
105
|
+
-> Easy enough. Just type "exit", or "quit". Or press ctrl-D.
|
106
|
+
|
107
|
+
"I want to execute a command on all servers."
|
108
|
+
-> Just type the command, and press enter. It will be passed,
|
109
|
+
verbatim, to all defined servers.
|
110
|
+
|
111
|
+
"What if I only want it to execute on a subset of them?"
|
112
|
+
-> No problem, just specify the list of servers, separated by
|
113
|
+
commas, before the command, with the `on' keyword:
|
114
|
+
|
115
|
+
cap> on app1.foo.com,app2.foo.com echo ping
|
116
|
+
|
117
|
+
"Nice, but can I specify the servers by role?"
|
118
|
+
-> You sure can. Just use the `with' keyword, followed by the
|
119
|
+
comma-delimited list of role names:
|
120
|
+
|
121
|
+
cap> with app,db echo ping
|
122
|
+
|
123
|
+
"Can I execute a Capistrano task from within this shell?"
|
124
|
+
-> Yup. Just prefix the task with an exclamation mark:
|
125
|
+
|
126
|
+
cap> !deploy
|
127
|
+
HELP
|
128
|
+
end
|
129
|
+
|
130
|
+
# Determine which servers the given task requires a connection to, and
|
131
|
+
# establish connections to them if necessary. Return the list of
|
132
|
+
# servers (names).
|
133
|
+
def connect(task)
|
134
|
+
servers = configuration.find_servers_for_task(task)
|
135
|
+
needing_connections = servers - configuration.sessions.keys
|
136
|
+
unless needing_connections.empty?
|
137
|
+
puts "[establishing connection(s) to #{needing_connections.join(', ')}]"
|
138
|
+
configuration.establish_connections_to(needing_connections)
|
139
|
+
end
|
140
|
+
servers
|
141
|
+
end
|
142
|
+
|
143
|
+
# Execute the given command. If the command is prefixed by an exclamation
|
144
|
+
# mark, it is assumed to refer to another capistrano task, which will
|
145
|
+
# be invoked. Otherwise, it is executed as a command on all associated
|
146
|
+
# servers.
|
147
|
+
def exec(command)
|
148
|
+
@mutex.synchronize do
|
149
|
+
if command[0] == ?!
|
150
|
+
exec_tasks(command[1..-1].split)
|
151
|
+
else
|
152
|
+
servers = connect(configuration.current_task)
|
153
|
+
exec_command(command, servers)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
ensure
|
157
|
+
STDOUT.flush
|
158
|
+
end
|
159
|
+
|
160
|
+
# Given an array of task names, invoke them in sequence.
|
161
|
+
def exec_tasks(list)
|
162
|
+
list.each do |task_name|
|
163
|
+
task = configuration.find_task(task_name)
|
164
|
+
raise Capistrano::NoSuchTaskError, "no such task `#{task_name}'" unless task
|
165
|
+
connect(task)
|
166
|
+
configuration.execute_task(task)
|
167
|
+
end
|
168
|
+
rescue Capistrano::NoMatchingServersError, Capistrano::NoSuchTaskError => error
|
169
|
+
warn "error: #{error.message}"
|
170
|
+
end
|
171
|
+
|
172
|
+
# Execute a command on the given list of servers.
|
173
|
+
def exec_command(command, servers)
|
174
|
+
command = command.gsub(/\bsudo\b/, "sudo -p '#{configuration.sudo_prompt}'")
|
175
|
+
processor = configuration.sudo_behavior_callback(Configuration.default_io_proc)
|
176
|
+
sessions = servers.map { |server| configuration.sessions[server] }
|
177
|
+
options = configuration.add_default_command_options({})
|
178
|
+
cmd = Command.new(command, sessions, options.merge(:logger => configuration.logger), &processor)
|
179
|
+
previous = trap("INT") { cmd.stop! }
|
180
|
+
cmd.process!
|
181
|
+
rescue Capistrano::Error => error
|
182
|
+
warn "error: #{error.message}"
|
183
|
+
ensure
|
184
|
+
trap("INT", previous)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Return the object that will be used to query input from the console.
|
188
|
+
# The returned object will quack (more or less) like Readline.
|
189
|
+
def reader
|
190
|
+
@reader ||= begin
|
191
|
+
require 'readline'
|
192
|
+
Readline
|
193
|
+
rescue LoadError
|
194
|
+
ReadlineFallback
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Prepare every little thing for the shell. Starts the background
|
199
|
+
# thread and generally gets things ready for the REPL.
|
200
|
+
def setup
|
201
|
+
configuration.logger.level = Capistrano::Logger::INFO
|
202
|
+
|
203
|
+
@mutex = Mutex.new
|
204
|
+
@bgthread = Thread.new do
|
205
|
+
loop do
|
206
|
+
@mutex.synchronize { process_iteration(0.1) }
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Set the given option to +value+.
|
212
|
+
def set_option(opt, value)
|
213
|
+
case opt
|
214
|
+
when "v" then
|
215
|
+
puts "setting log verbosity to #{value.to_i}"
|
216
|
+
configuration.logger.level = value.to_i
|
217
|
+
when "o" then
|
218
|
+
case value
|
219
|
+
when "vi" then
|
220
|
+
puts "using vi edit mode"
|
221
|
+
reader.vi_editing_mode
|
222
|
+
when "emacs" then
|
223
|
+
puts "using emacs edit mode"
|
224
|
+
reader.emacs_editing_mode
|
225
|
+
else
|
226
|
+
puts "unknown -o option #{value.inspect}"
|
227
|
+
end
|
228
|
+
else
|
229
|
+
puts "unknown setting #{opt.inspect}"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Process a command. Interprets the scope_type (must be nil, "with", or
|
234
|
+
# "on") and the command. If no command is given, then the scope is made
|
235
|
+
# effective for all subsequent commands. If the scope value is "all",
|
236
|
+
# then the scope is unrestricted.
|
237
|
+
def process_command(scope_type, scope_value, command)
|
238
|
+
env_var = case scope_type
|
239
|
+
when "with" then "ROLES"
|
240
|
+
when "on" then "HOSTS"
|
241
|
+
end
|
242
|
+
|
243
|
+
old_var, ENV[env_var] = ENV[env_var], (scope_value == "all" ? nil : scope_value) if env_var
|
244
|
+
if command
|
245
|
+
begin
|
246
|
+
exec(command)
|
247
|
+
ensure
|
248
|
+
ENV[env_var] = old_var if env_var
|
249
|
+
end
|
250
|
+
else
|
251
|
+
puts "scoping #{scope_type} #{scope_value}"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# All open sessions, needed to satisfy the Command::Processable include
|
257
|
+
def sessions
|
258
|
+
configuration.sessions.values
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
gem 'net-ssh', ">= 2.0.10"
|
4
|
+
rescue LoadError, NameError
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'net/ssh'
|
8
|
+
|
9
|
+
module Capistrano
|
10
|
+
# A helper class for dealing with SSH connections.
|
11
|
+
class SSH
|
12
|
+
# Patch an accessor onto an SSH connection so that we can record the server
|
13
|
+
# definition object that defines the connection. This is useful because
|
14
|
+
# the gateway returns connections whose "host" is 127.0.0.1, instead of
|
15
|
+
# the host on the other side of the tunnel.
|
16
|
+
module Server #:nodoc:
|
17
|
+
def self.apply_to(connection, server)
|
18
|
+
connection.extend(Server)
|
19
|
+
connection.xserver = server
|
20
|
+
connection
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_accessor :xserver
|
24
|
+
end
|
25
|
+
|
26
|
+
# An abstraction to make it possible to connect to the server via public key
|
27
|
+
# without prompting for the password. If the public key authentication fails
|
28
|
+
# this will fall back to password authentication.
|
29
|
+
#
|
30
|
+
# +server+ must be an instance of ServerDefinition.
|
31
|
+
#
|
32
|
+
# If a block is given, the new session is yielded to it, otherwise the new
|
33
|
+
# session is returned.
|
34
|
+
#
|
35
|
+
# If an :ssh_options key exists in +options+, it is passed to the Net::SSH
|
36
|
+
# constructor. Values in +options+ are then merged into it, and any
|
37
|
+
# connection information in +server+ is added last, so that +server+ info
|
38
|
+
# takes precedence over +options+, which takes precendence over ssh_options.
|
39
|
+
def self.connect(server, options={})
|
40
|
+
connection_strategy(server, options) do |host, user, connection_options|
|
41
|
+
connection = Net::SSH.start(host, user, connection_options)
|
42
|
+
Server.apply_to(connection, server)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Abstracts the logic for establishing an SSH connection (which includes
|
47
|
+
# testing for connection failures and retrying with a password, and so forth,
|
48
|
+
# mostly made complicated because of the fact that some of these variables
|
49
|
+
# might be lazily evaluated and try to do something like prompt the user,
|
50
|
+
# which should only happen when absolutely necessary.
|
51
|
+
#
|
52
|
+
# This will yield the hostname, username, and a hash of connection options
|
53
|
+
# to the given block, which should return a new connection.
|
54
|
+
def self.connection_strategy(server, options={}, &block)
|
55
|
+
methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ]
|
56
|
+
password_value = nil
|
57
|
+
|
58
|
+
# construct the hash of ssh options that should be passed more-or-less
|
59
|
+
# directly to Net::SSH. This will be the general ssh options, merged with
|
60
|
+
# the server-specific ssh-options.
|
61
|
+
ssh_options = (options[:ssh_options] || {}).merge(server.options[:ssh_options] || {})
|
62
|
+
|
63
|
+
# load any SSH configuration files that were specified in the SSH options. This
|
64
|
+
# will load from ~/.ssh/config and /etc/ssh_config by default (see Net::SSH
|
65
|
+
# for details). Merge the explicitly given ssh_options over the top of the info
|
66
|
+
# from the config file.
|
67
|
+
ssh_options = Net::SSH.configuration_for(server.host, ssh_options.fetch(:config, true)).merge(ssh_options)
|
68
|
+
|
69
|
+
# Once we've loaded the config, we don't need Net::SSH to do it again.
|
70
|
+
ssh_options[:config] = false
|
71
|
+
|
72
|
+
user = server.user || options[:user] || ssh_options[:username] ||
|
73
|
+
ssh_options[:user] || ServerDefinition.default_user
|
74
|
+
port = server.port || options[:port] || ssh_options[:port]
|
75
|
+
|
76
|
+
# the .ssh/config file might have changed the host-name on us
|
77
|
+
host = ssh_options.fetch(:host_name, server.host)
|
78
|
+
|
79
|
+
ssh_options[:port] = port if port
|
80
|
+
|
81
|
+
# delete these, since we've determined which username to use by this point
|
82
|
+
ssh_options.delete(:username)
|
83
|
+
ssh_options.delete(:user)
|
84
|
+
|
85
|
+
begin
|
86
|
+
connection_options = ssh_options.merge(
|
87
|
+
:password => password_value,
|
88
|
+
:auth_methods => ssh_options[:auth_methods] || methods.shift
|
89
|
+
)
|
90
|
+
|
91
|
+
yield host, user, connection_options
|
92
|
+
rescue Net::SSH::AuthenticationFailed
|
93
|
+
raise if methods.empty? || ssh_options[:auth_methods]
|
94
|
+
password_value = options[:password]
|
95
|
+
retry
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'capistrano/server_definition'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
# Represents the definition of a single task.
|
5
|
+
class TaskDefinition
|
6
|
+
attr_reader :name, :namespace, :options, :body, :desc, :on_error, :max_hosts
|
7
|
+
|
8
|
+
def initialize(name, namespace, options={}, &block)
|
9
|
+
@name, @namespace, @options = name, namespace, options
|
10
|
+
@desc = @options.delete(:desc)
|
11
|
+
@on_error = options.delete(:on_error)
|
12
|
+
@max_hosts = options[:max_hosts] && options[:max_hosts].to_i
|
13
|
+
@body = block or raise ArgumentError, "a task requires a block"
|
14
|
+
@servers = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the task's fully-qualified name, including the namespace
|
18
|
+
def fully_qualified_name
|
19
|
+
@fully_qualified_name ||= begin
|
20
|
+
if namespace.default_task == self
|
21
|
+
namespace.fully_qualified_name
|
22
|
+
else
|
23
|
+
[namespace.fully_qualified_name, name].compact.join(":")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the description for this task, with newlines collapsed and
|
29
|
+
# whitespace stripped. Returns the empty string if there is no
|
30
|
+
# description for this task.
|
31
|
+
def description(rebuild=false)
|
32
|
+
@description = nil if rebuild
|
33
|
+
@description ||= begin
|
34
|
+
description = @desc || ""
|
35
|
+
|
36
|
+
indentation = description[/\A\s+/]
|
37
|
+
if indentation
|
38
|
+
reformatted_description = ""
|
39
|
+
description.strip.each_line do |line|
|
40
|
+
line = line.chomp.sub(/^#{indentation}/, "")
|
41
|
+
line = line.gsub(/#{indentation}\s*/, " ") if line[/^\S/]
|
42
|
+
reformatted_description << line << "\n"
|
43
|
+
end
|
44
|
+
description = reformatted_description
|
45
|
+
end
|
46
|
+
|
47
|
+
description.strip.gsub(/\r\n/, "\n")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the first sentence of the full description. If +max_length+ is
|
52
|
+
# given, the result will be truncated if it is longer than +max_length+,
|
53
|
+
# and an ellipsis appended.
|
54
|
+
def brief_description(max_length=nil)
|
55
|
+
brief = description[/^.*?\.(?=\s|$)/] || description
|
56
|
+
|
57
|
+
if max_length && brief.length > max_length
|
58
|
+
brief = brief[0,max_length-3] + "..."
|
59
|
+
end
|
60
|
+
|
61
|
+
brief
|
62
|
+
end
|
63
|
+
|
64
|
+
# Indicates whether the task wants to continue, even if a server has failed
|
65
|
+
# previously
|
66
|
+
def continue_on_error?
|
67
|
+
@on_error == :continue
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|