capissh 0.0.1
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/LICENSE +18 -0
- data/README.md +37 -0
- data/lib/capissh.rb +43 -0
- data/lib/capissh/command.rb +197 -0
- data/lib/capissh/command/tree.rb +138 -0
- data/lib/capissh/configuration.rb +65 -0
- data/lib/capissh/connection_manager.rb +250 -0
- data/lib/capissh/errors.rb +19 -0
- data/lib/capissh/file_transfers.rb +54 -0
- data/lib/capissh/invocation.rb +278 -0
- data/lib/capissh/logger.rb +157 -0
- data/lib/capissh/processable.rb +54 -0
- data/lib/capissh/server_definition.rb +111 -0
- data/lib/capissh/ssh.rb +110 -0
- data/lib/capissh/transfer.rb +218 -0
- data/lib/capissh/version.rb +3 -0
- metadata +204 -0
@@ -0,0 +1,250 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
require 'net/ssh/gateway'
|
3
|
+
require 'capissh/ssh'
|
4
|
+
require 'capissh/errors'
|
5
|
+
require 'capissh/server_definition'
|
6
|
+
require 'capissh/logger'
|
7
|
+
|
8
|
+
module Capissh
|
9
|
+
class ConnectionManager
|
10
|
+
class DefaultConnectionFactory
|
11
|
+
def initialize(options)
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def connect_to(server)
|
16
|
+
SSH.connect(server, @options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class GatewayConnectionFactory
|
21
|
+
def initialize(gateway, options)
|
22
|
+
@options = options
|
23
|
+
Thread.abort_on_exception = true
|
24
|
+
@gateways = {}
|
25
|
+
@default_gateway = nil
|
26
|
+
if gateway.is_a?(Hash)
|
27
|
+
@options[:logger].debug "Creating multiple gateways using #{gateway.inspect}" if @options[:logger]
|
28
|
+
gateway.each do |gw, hosts|
|
29
|
+
gateway_connection = add_gateway(gw)
|
30
|
+
Array(hosts).each do |host|
|
31
|
+
# Why is the default only set if there's at least one host?
|
32
|
+
# It seems odd enough to be intentional, since it could easily be set outside of the loop.
|
33
|
+
@default_gateway ||= gateway_connection
|
34
|
+
@gateways[host] = gateway_connection
|
35
|
+
end
|
36
|
+
end
|
37
|
+
else
|
38
|
+
@options[:logger].debug "Creating gateway using #{Array(gateway).join(', ')}" if @options[:logger]
|
39
|
+
@default_gateway = add_gateway(gateway)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_gateway(gateway)
|
44
|
+
gateways = ServerDefinition.wrap_list(gateway)
|
45
|
+
|
46
|
+
tunnel = SSH.gateway(gateways.shift, @options)
|
47
|
+
|
48
|
+
gateways.inject(tunnel) do |tunnel, destination|
|
49
|
+
@options[:logger].debug "Creating tunnel to #{destination}" if @options[:logger]
|
50
|
+
local = local_host(destination, tunnel)
|
51
|
+
SSH.gateway(local, @options)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def connect_to(server)
|
56
|
+
@options[:logger].debug "establishing connection to `#{server}' via gateway" if @options[:logger]
|
57
|
+
local = local_host(server, gateway_for(server))
|
58
|
+
session = SSH.connect(local, @options)
|
59
|
+
session.xserver = server
|
60
|
+
session
|
61
|
+
end
|
62
|
+
|
63
|
+
def local_host(destination, tunnel)
|
64
|
+
ServerDefinition.new("127.0.0.1", :user => destination.user, :port => tunnel.open(destination.host, destination.connect_to_port))
|
65
|
+
end
|
66
|
+
|
67
|
+
def gateway_for(server)
|
68
|
+
@gateways[server.host] || @default_gateway
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Instantiates a new ConnectionManager object.
|
73
|
+
# +options+ must be a hash containing any of the following keys:
|
74
|
+
#
|
75
|
+
# * +gateway+: (optional), ssh gateway
|
76
|
+
# * +logger+: (optional), a Capissh::Logger instance
|
77
|
+
# * +ssh_options+: (optional), options for Net::SSH
|
78
|
+
# * +verbose+: (optional), verbosity level for ssh
|
79
|
+
# * +user+: (optional), user for all servers
|
80
|
+
# * +port+: (optional), port for all servers
|
81
|
+
# * +password+: (optional), password for all servers
|
82
|
+
#
|
83
|
+
# * many more that I haven't written yet
|
84
|
+
#
|
85
|
+
def initialize(options={})
|
86
|
+
@options = options.dup
|
87
|
+
@gateway = @options.delete(:gateway)
|
88
|
+
@logger = @options[:logger] || Capissh::Logger.default
|
89
|
+
Thread.current[:sessions] = {}
|
90
|
+
Thread.current[:failed_sessions] = []
|
91
|
+
end
|
92
|
+
|
93
|
+
attr_reader :logger
|
94
|
+
|
95
|
+
# A hash of the SSH sessions that are currently open and available.
|
96
|
+
# Because sessions are constructed lazily, this will only contain
|
97
|
+
# connections to those servers that have been the targets of one or more
|
98
|
+
# executed tasks. Stored on a per-thread basis to improve thread-safety.
|
99
|
+
def sessions
|
100
|
+
Thread.current[:sessions] ||= {}
|
101
|
+
end
|
102
|
+
|
103
|
+
# Indicate that the given server could not be connected to.
|
104
|
+
def failed!(server)
|
105
|
+
Thread.current[:failed_sessions] << server
|
106
|
+
end
|
107
|
+
|
108
|
+
# Query whether previous connection attempts to the given server have
|
109
|
+
# failed.
|
110
|
+
def has_failed?(server)
|
111
|
+
Thread.current[:failed_sessions].include?(server)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Used to force connections to be made to the current task's servers.
|
115
|
+
# Connections are normally made lazily in Capissh--you can use this
|
116
|
+
# to force them open before performing some operation that might be
|
117
|
+
# time-sensitive.
|
118
|
+
def connect!(servers, options={})
|
119
|
+
execute_on_servers(servers, options) { }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns the object responsible for establishing new SSH connections.
|
123
|
+
# The factory will respond to #connect_to, which can be used to
|
124
|
+
# establish connections to servers defined via ServerDefinition objects.
|
125
|
+
def connection_factory
|
126
|
+
@connection_factory ||= begin
|
127
|
+
if @gateway
|
128
|
+
logger.debug "establishing connection to gateway `#{@gateway.inspect}'"
|
129
|
+
GatewayConnectionFactory.new(@gateway, @options)
|
130
|
+
else
|
131
|
+
DefaultConnectionFactory.new(@options)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Ensures that there are active sessions for each server in the list.
|
137
|
+
def establish_connections_to(servers)
|
138
|
+
# force the connection factory to be instantiated synchronously,
|
139
|
+
# otherwise we wind up with multiple gateway instances, because
|
140
|
+
# each connection is done in parallel.
|
141
|
+
connection_factory
|
142
|
+
|
143
|
+
failed_servers = []
|
144
|
+
servers_array = Array(servers)
|
145
|
+
|
146
|
+
threads = servers_array.map { |server| establish_connection_to(server, failed_servers) }
|
147
|
+
threads.each { |t| t.join }
|
148
|
+
|
149
|
+
if failed_servers.any?
|
150
|
+
messages = failed_servers.map { |h| "#{h[:server]} (#{h[:error].class}: #{h[:error].message})" }
|
151
|
+
error = ConnectionError.new("connection failed for: #{messages.join(', ')}")
|
152
|
+
error.hosts = failed_servers.map { |h| h[:server] }.each {|server| failed!(server) }
|
153
|
+
raise error
|
154
|
+
end
|
155
|
+
|
156
|
+
servers_array.map {|server| sessions[server] }
|
157
|
+
end
|
158
|
+
|
159
|
+
# Destroys sessions for each server in the list.
|
160
|
+
def teardown_connections_to(servers)
|
161
|
+
servers.each do |server|
|
162
|
+
begin
|
163
|
+
sessions.delete(server).close
|
164
|
+
rescue IOError
|
165
|
+
# the TCP connection is already dead
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Determines the set of servers and establishes connections to them,
|
171
|
+
# and then yields that list of servers.
|
172
|
+
#
|
173
|
+
# All options will be used to find servers. (see find_servers)
|
174
|
+
#
|
175
|
+
# The additional options below will also be used as follows:
|
176
|
+
#
|
177
|
+
# * +on_no_matching_servers+: (optional), set to :continue to return
|
178
|
+
# instead of raising when no servers are found
|
179
|
+
# * +max_hosts+: (optional), integer to batch commands in chunks of hosts
|
180
|
+
# * +continue_on_error+: (optionsal), continue on connection errors
|
181
|
+
#
|
182
|
+
def execute_on_servers(servers, options={}, &block)
|
183
|
+
raise ArgumentError, "expected a block" unless block_given?
|
184
|
+
|
185
|
+
connect_to_servers = ServerDefinition.wrap_list(*servers)
|
186
|
+
|
187
|
+
if options[:continue_on_error]
|
188
|
+
connect_to_servers.delete_if { |s| has_failed?(s) }
|
189
|
+
end
|
190
|
+
|
191
|
+
if connect_to_servers.empty?
|
192
|
+
raise Capissh::NoMatchingServersError, "no servers found to match #{options.inspect}" unless options[:continue_on_no_matching_servers]
|
193
|
+
return
|
194
|
+
end
|
195
|
+
|
196
|
+
logger.trace "servers: #{connect_to_servers.map { |s| s.host }.inspect}"
|
197
|
+
|
198
|
+
max_hosts = (options[:max_hosts] || connect_to_servers.size).to_i
|
199
|
+
is_subset = max_hosts < connect_to_servers.size
|
200
|
+
|
201
|
+
if max_hosts <= 0
|
202
|
+
raise Capissh::NoMatchingServersError, "max_hosts is invalid for #{options.inspect}" unless options[:continue_on_no_matching_servers]
|
203
|
+
return
|
204
|
+
end
|
205
|
+
|
206
|
+
# establish connections to those servers in groups of max_hosts, as necessary
|
207
|
+
connect_to_servers.each_slice(max_hosts) do |servers_slice|
|
208
|
+
begin
|
209
|
+
establish_connections_to(servers_slice)
|
210
|
+
rescue ConnectionError => error
|
211
|
+
raise error unless options[:continue_on_error]
|
212
|
+
error.hosts.each do |h|
|
213
|
+
servers_slice.delete(h)
|
214
|
+
failed!(h)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
begin
|
219
|
+
yield servers_slice.map { |server| sessions[server] }
|
220
|
+
rescue RemoteError => error
|
221
|
+
raise error unless options[:continue_on_error]
|
222
|
+
error.hosts.each { |h| failed!(h) }
|
223
|
+
end
|
224
|
+
|
225
|
+
# if dealing with a subset (e.g., :max_hosts is less than the
|
226
|
+
# number of servers available) teardown the subset of connections
|
227
|
+
# that were just made, so that we can make room for the next subset.
|
228
|
+
teardown_connections_to(servers_slice) if is_subset
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
private
|
233
|
+
|
234
|
+
# We establish the connection by creating a thread in a new method--this
|
235
|
+
# prevents problems with the thread's scope seeing the wrong 'server'
|
236
|
+
# variable if the thread just happens to take too long to start up.
|
237
|
+
def establish_connection_to(server, failures=nil)
|
238
|
+
current_thread = Thread.current
|
239
|
+
Thread.new { safely_establish_connection_to(server, current_thread, failures) }
|
240
|
+
end
|
241
|
+
|
242
|
+
def safely_establish_connection_to(server, thread, failures=nil)
|
243
|
+
thread[:sessions] ||= {} # can this move up to the current_thread part above?
|
244
|
+
thread[:sessions][server] ||= connection_factory.connect_to(server)
|
245
|
+
rescue Exception => err
|
246
|
+
raise unless failures
|
247
|
+
failures << { :server => server, :error => err }
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Capissh
|
2
|
+
|
3
|
+
Error = Class.new(RuntimeError)
|
4
|
+
|
5
|
+
CaptureError = Class.new(Capissh::Error)
|
6
|
+
NoSuchTaskError = Class.new(Capissh::Error)
|
7
|
+
NoMatchingServersError = Class.new(Capissh::Error)
|
8
|
+
|
9
|
+
class RemoteError < Error
|
10
|
+
attr_accessor :hosts
|
11
|
+
end
|
12
|
+
|
13
|
+
ConnectionError = Class.new(Capissh::RemoteError)
|
14
|
+
TransferError = Class.new(Capissh::RemoteError)
|
15
|
+
CommandError = Class.new(Capissh::RemoteError)
|
16
|
+
|
17
|
+
LocalArgumentError = Class.new(Capissh::Error)
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'capissh/transfer'
|
2
|
+
|
3
|
+
module Capissh
|
4
|
+
class FileTransfers
|
5
|
+
attr_reader :configuration, :connection_manager, :logger
|
6
|
+
|
7
|
+
def initialize(configuration, connection_manager, options={})
|
8
|
+
@configuration = configuration
|
9
|
+
@connection_manager = connection_manager
|
10
|
+
@logger = options[:logger]
|
11
|
+
end
|
12
|
+
|
13
|
+
# Store the given data at the given location on all servers targetted
|
14
|
+
# by the current task. If <tt>:mode</tt> is specified it is used to
|
15
|
+
# set the mode on the file.
|
16
|
+
def put(servers, data, path, options={})
|
17
|
+
upload(servers, StringIO.new(data), path, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get file remote_path from FIRST server targeted by
|
21
|
+
# the current task and transfer it to local machine as path.
|
22
|
+
#
|
23
|
+
# Pass only one server, or the first of the set of servers will be used.
|
24
|
+
#
|
25
|
+
# get server, "#{deploy_to}/current/log/production.log", "log/production.log.web"
|
26
|
+
def get(servers, remote_path, path, options={}, &block)
|
27
|
+
download(Array(servers).slice(0,1), remote_path, path, options, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def upload(servers, from, to, options={}, &block)
|
31
|
+
opts = options.dup
|
32
|
+
mode = opts.delete(:mode)
|
33
|
+
transfer(servers, :up, from, to, opts, &block)
|
34
|
+
if mode
|
35
|
+
mode = mode.is_a?(Numeric) ? mode.to_s(8) : mode.to_s
|
36
|
+
configuration.run servers, "chmod #{mode} #{to}", opts
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def download(servers, from, to, options={}, &block)
|
41
|
+
transfer(servers, :down, from, to, options, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def transfer(servers, direction, from, to, options={}, &block)
|
45
|
+
if configuration.dry_run
|
46
|
+
return logger.debug "transfering: #{[direction, from, to] * ', '}"
|
47
|
+
end
|
48
|
+
connection_manager.execute_on_servers(servers, options) do |sessions|
|
49
|
+
Transfer.process(direction, from, to, sessions, options.merge(:logger => logger), &block)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,278 @@
|
|
1
|
+
require 'capissh/command'
|
2
|
+
|
3
|
+
module Capissh
|
4
|
+
class Invocation
|
5
|
+
attr_reader :configuration, :logger, :connection_manager
|
6
|
+
|
7
|
+
def initialize(configuration, connection_manager, options={})
|
8
|
+
@configuration = configuration
|
9
|
+
@connection_manager = connection_manager
|
10
|
+
@logger = options[:logger]
|
11
|
+
end
|
12
|
+
|
13
|
+
# Executes different commands in parallel. This is useful for commands
|
14
|
+
# that need to be different on different hosts, but which could be
|
15
|
+
# otherwise run in parallel.
|
16
|
+
#
|
17
|
+
# The +options+ parameter is currently unused.
|
18
|
+
#
|
19
|
+
# Example:
|
20
|
+
#
|
21
|
+
# parallel do |session|
|
22
|
+
# session.when "in?(:app)", "/path/to/restart/mongrel"
|
23
|
+
# session.when "in?(:web)", "/path/to/restart/apache"
|
24
|
+
# session.when "in?(:db)", "/path/to/restart/mysql"
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# Each command may have its own callback block, for capturing and
|
28
|
+
# responding to output, with semantics identical to #run:
|
29
|
+
#
|
30
|
+
# session.when "in?(:app)", "/path/to/restart/mongrel" do |ch, stream, data|
|
31
|
+
# # ch is the SSH channel for this command, used to send data
|
32
|
+
# # back to the command (e.g. ch.send_data("password\n"))
|
33
|
+
# # stream is either :out or :err, for which stream the data arrived on
|
34
|
+
# # data is a string containing data sent from the remote command
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# Also, you can specify a fallback command, to use when none of the
|
38
|
+
# conditions match a server:
|
39
|
+
#
|
40
|
+
# session.else "/execute/something/else"
|
41
|
+
#
|
42
|
+
# The string specified as the first argument to +when+ may be any valid
|
43
|
+
# Ruby code. It has access to the following variables and methods:
|
44
|
+
#
|
45
|
+
# * +server+ is the ServerDefinition object for the server. This can be
|
46
|
+
# used to get the host-name, etc.
|
47
|
+
# * +configuration+ is the current Capissh::Configuration object, which
|
48
|
+
# you can use to get the value of variables, etc.
|
49
|
+
#
|
50
|
+
# For example:
|
51
|
+
#
|
52
|
+
# session.when "server.host =~ /app/", "/some/command"
|
53
|
+
# session.when "server.host == configuration[:some_var]", "/another/command"
|
54
|
+
# session.when "server.role?(:web) || server.role?(:app)", "/more/commands"
|
55
|
+
#
|
56
|
+
# See #run for a description of the valid +options+.
|
57
|
+
def parallel(servers, options={})
|
58
|
+
raise ArgumentError, "parallel() requires a block" unless block_given?
|
59
|
+
tree = Command::Tree.new(configuration) { |t| yield t }
|
60
|
+
run_tree(servers, tree, options)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Invokes the given command. If a +via+ key is given, it will be used
|
64
|
+
# to determine what method to use to invoke the command. It defaults
|
65
|
+
# to :run, but may be :sudo, or any other method that conforms to the
|
66
|
+
# same interface as run and sudo.
|
67
|
+
def invoke_command(servers, cmd, options={}, &block)
|
68
|
+
options = options.dup
|
69
|
+
via = options.delete(:via) || :run
|
70
|
+
send(via, servers, cmd, options, &block)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Execute the given command on all servers that are the target of the
|
74
|
+
# current task. If a block is given, it is invoked for all output
|
75
|
+
# generated by the command, and should accept three parameters: the SSH
|
76
|
+
# channel (which may be used to send data back to the remote process),
|
77
|
+
# the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
|
78
|
+
# stdout), and the data that was received.
|
79
|
+
#
|
80
|
+
# The +options+ hash may include any of the following keys:
|
81
|
+
#
|
82
|
+
# * :on_no_matching_servers - if :continue, will continue to execute tasks if
|
83
|
+
# no matching servers are found for the host criteria. The default is to raise
|
84
|
+
# a NoMatchingServersError exception.
|
85
|
+
# * :max_hosts - specifies the maximum number of hosts that should be selected
|
86
|
+
# at a time. If this value is less than the number of hosts that are selected
|
87
|
+
# to run, then the hosts will be run in groups of max_hosts. The default is nil,
|
88
|
+
# which indicates that there is no maximum host limit. Please note this does not
|
89
|
+
# limit the number of SSH channels that can be open, only the number of hosts upon
|
90
|
+
# which this will be called.
|
91
|
+
# * :shell - says which shell should be used to invoke commands. This
|
92
|
+
# defaults to "sh". Setting this to false causes Capissh to invoke
|
93
|
+
# the commands directly, without wrapping them in a shell invocation.
|
94
|
+
# * :data - if not nil (the default), this should be a string that will
|
95
|
+
# be passed to the command's stdin stream.
|
96
|
+
# * :pty - if true, a pseudo-tty will be allocated for each command. The
|
97
|
+
# default is false. Note that there are benefits and drawbacks both ways.
|
98
|
+
# Empirically, it appears that if a pty is allocated, the SSH server daemon
|
99
|
+
# will _not_ read user shell start-up scripts (e.g. bashrc, etc.). However,
|
100
|
+
# if a pty is _not_ allocated, some commands will refuse to run in
|
101
|
+
# interactive mode and will not prompt for (e.g.) passwords.
|
102
|
+
# * :env - a hash of environment variable mappings that should be made
|
103
|
+
# available to the command. The keys should be environment variable names,
|
104
|
+
# and the values should be their corresponding values. The default is
|
105
|
+
# empty, but may be modified by changing the +default_environment+
|
106
|
+
# Capissh variable.
|
107
|
+
# * :eof - if true, the standard input stream will be closed after sending
|
108
|
+
# any data specified in the :data option. If false, the input stream is
|
109
|
+
# left open. The default is to close the input stream only if no block is
|
110
|
+
# passed.
|
111
|
+
#
|
112
|
+
# Note that if you set these keys in the +default_run_options+ Capissh
|
113
|
+
# variable, they will apply for all invocations of #run, #invoke_command,
|
114
|
+
# and #parallel.
|
115
|
+
def run(servers, cmd, options={}, &block)
|
116
|
+
if options[:eof].nil? && !cmd.include?(sudo_command)
|
117
|
+
options = options.merge(:eof => !block_given?)
|
118
|
+
end
|
119
|
+
block ||= Command.default_io_proc
|
120
|
+
tree = Command::Tree.twig(configuration, cmd, &block)
|
121
|
+
run_tree(servers, tree, options)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Executes a Capissh::Command::Tree object. This is not for direct
|
125
|
+
# use, but should instead be called indirectly, via #run or #parallel,
|
126
|
+
# or #invoke_command.
|
127
|
+
def run_tree(servers, tree, options={})
|
128
|
+
if tree.branches.empty? && tree.fallback
|
129
|
+
logger.debug "executing #{tree.fallback}" unless options[:silent]
|
130
|
+
elsif tree.branches.any?
|
131
|
+
logger.debug "executing multiple commands in parallel"
|
132
|
+
tree.each do |branch|
|
133
|
+
logger.trace "-> #{branch}"
|
134
|
+
end
|
135
|
+
else
|
136
|
+
raise ArgumentError, "attempt to execute without specifying a command"
|
137
|
+
end
|
138
|
+
|
139
|
+
return if configuration.dry_run || (configuration.debug && continue_execution(tree) == false)
|
140
|
+
|
141
|
+
options = add_default_command_options(options)
|
142
|
+
|
143
|
+
tree.each do |branch|
|
144
|
+
if branch.command.include?(sudo_command)
|
145
|
+
branch.callback = sudo_behavior_callback(branch.callback)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
connection_manager.execute_on_servers(servers, options) do |sessions|
|
150
|
+
Command.process(tree, sessions, options.merge(:logger => logger))
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Invoked like #run, but executing the command via sudo.
|
155
|
+
# This assumes that the sudo password (if required) is the same as the
|
156
|
+
# password for logging in to the server.
|
157
|
+
#
|
158
|
+
# sudo "mkdir /path/to/dir"
|
159
|
+
#
|
160
|
+
# Also, this method understands a <tt>:sudo</tt> configuration variable,
|
161
|
+
# which (if specified) will be used as the full path to the sudo
|
162
|
+
# executable on the remote machine:
|
163
|
+
#
|
164
|
+
# Capissh.new(sudo: "/opt/local/bin/sudo")
|
165
|
+
#
|
166
|
+
# If you know what you're doing, you can also set <tt>:sudo_prompt</tt>,
|
167
|
+
# which tells capissh which prompt sudo should use when asking for
|
168
|
+
# a password. (This is so that capissh knows what prompt to look for
|
169
|
+
# in the output.) If you set :sudo_prompt to an empty string, Capissh
|
170
|
+
# will not send a preferred prompt.
|
171
|
+
def sudo(servers, command, options={}, &block)
|
172
|
+
run(servers, "#{sudo_command(options)} #{command}", options, &block)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns the command string used by capissh to invoke a comamnd via
|
176
|
+
# sudo.
|
177
|
+
#
|
178
|
+
# run "#{sudo_command :as => 'bob'} mkdir /path/to/dir"
|
179
|
+
#
|
180
|
+
# Also, this method understands a <tt>:sudo</tt> configuration variable,
|
181
|
+
# which (if specified) will be used as the full path to the sudo
|
182
|
+
# executable on the remote machine:
|
183
|
+
#
|
184
|
+
# Capissh.new(sudo: "/opt/local/bin/sudo")
|
185
|
+
#
|
186
|
+
# If you know what you're doing, you can also set <tt>:sudo_prompt</tt>,
|
187
|
+
# which tells capissh which prompt sudo should use when asking for
|
188
|
+
# a password. (This is so that capissh knows what prompt to look for
|
189
|
+
# in the output.) If you set :sudo_prompt to an empty string, Capissh
|
190
|
+
# will not send a preferred prompt.
|
191
|
+
def sudo_command(options={}, &block)
|
192
|
+
user = options[:as] && "-u #{options.delete(:as)}"
|
193
|
+
|
194
|
+
sudo_prompt_option = "-p '#{sudo_prompt}'" unless sudo_prompt.empty?
|
195
|
+
[configuration.fetch(:sudo, "sudo"), sudo_prompt_option, user].compact.join(" ")
|
196
|
+
end
|
197
|
+
|
198
|
+
# tests are too invasive right now
|
199
|
+
# protected
|
200
|
+
|
201
|
+
# Returns a Proc object that defines the behavior of the sudo
|
202
|
+
# callback. The returned Proc will defer to the +fallback+ argument
|
203
|
+
# (which should also be a Proc) for any output it does not
|
204
|
+
# explicitly handle.
|
205
|
+
def sudo_behavior_callback(fallback)
|
206
|
+
# in order to prevent _each host_ from prompting when the password
|
207
|
+
# was wrong, let's track which host prompted first and only allow
|
208
|
+
# subsequent prompts from that host.
|
209
|
+
prompt_host = nil
|
210
|
+
|
211
|
+
Proc.new do |ch, stream, out|
|
212
|
+
if out =~ /^Sorry, try again/
|
213
|
+
if prompt_host.nil? || prompt_host == ch[:server]
|
214
|
+
prompt_host = ch[:server]
|
215
|
+
logger.important out, "#{stream} :: #{ch[:server]}"
|
216
|
+
reset! :password
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
if out =~ /^#{Regexp.escape(sudo_prompt)}/
|
221
|
+
ch.send_data "#{configuration.fetch(:password,nil)}\n"
|
222
|
+
elsif fallback
|
223
|
+
fallback.call(ch, stream, out)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Merges the various default command options into the options hash and
|
229
|
+
# returns the result. The default command options that are understand
|
230
|
+
# are:
|
231
|
+
#
|
232
|
+
# * :default_environment: If the :env key already exists, the :env
|
233
|
+
# key is merged into default_environment and then added back into
|
234
|
+
# options.
|
235
|
+
# * :default_shell: if the :shell key already exists, it will be used.
|
236
|
+
# Otherwise, if the :default_shell key exists in the configuration,
|
237
|
+
# it will be used. Otherwise, no :shell key is added.
|
238
|
+
def add_default_command_options(options)
|
239
|
+
defaults = configuration.fetch(:default_run_options, {})
|
240
|
+
options = defaults.merge(options)
|
241
|
+
|
242
|
+
env = configuration.fetch(:default_environment, {})
|
243
|
+
env = env.merge(options[:env]) if options[:env]
|
244
|
+
options[:env] = env unless env.empty?
|
245
|
+
|
246
|
+
shell = options[:shell] || configuration.fetch(:default_shell, nil)
|
247
|
+
options[:shell] = shell unless shell.nil?
|
248
|
+
|
249
|
+
options
|
250
|
+
end
|
251
|
+
|
252
|
+
# Returns the prompt text to use with sudo
|
253
|
+
def sudo_prompt
|
254
|
+
configuration.fetch(:sudo_prompt, "sudo password: ")
|
255
|
+
end
|
256
|
+
|
257
|
+
def continue_execution(tree)
|
258
|
+
if tree.branches.length == 1
|
259
|
+
continue_execution_for_branch(tree.branches.first)
|
260
|
+
else
|
261
|
+
tree.each { |branch| branch.skip! unless continue_execution_for_branch(branch) }
|
262
|
+
tree.any? { |branch| !branch.skip? }
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def continue_execution_for_branch(branch)
|
267
|
+
case Capissh::CLI.debug_prompt(branch)
|
268
|
+
when "y"
|
269
|
+
true
|
270
|
+
when "n"
|
271
|
+
false
|
272
|
+
when "a"
|
273
|
+
exit(-1)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
278
|
+
end
|