capissh 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|